Socket
Socket
Sign inDemoInstall

sillytavern

Package Overview
Dependencies
Maintainers
1
Versions
67
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sillytavern - npm Package Compare versions

Comparing version 1.6.8 to 1.7.0

public/i18n.json

4

.github/ISSUE_TEMPLATE/bug_report.md

@@ -12,3 +12,3 @@ ---

**Have you searched for similar [bugs](https://github.com/Cohee1207/SillyTavern/issues?q=)?**
**Have you searched for similar [bugs](https://github.com/SillyTavern/SillyTavern/issues?q=)?**
Yes/No

@@ -41,3 +41,3 @@

- Browser [e.g. chrome, safari]
- Generation API [e.g. KoboldAI, OpenAI]
- Generation API [e.g. KoboldAI, OpenAI]
- Branch [main, dev]

@@ -44,0 +44,0 @@ - Model [e.g. Pygmalion 6b, LLaMa 13b]

@@ -10,3 +10,3 @@ ---

**Have you searched for similar [requests](https://github.com/Cohee1207/SillyTavern/issues?q=)?**
**Have you searched for similar [requests](https://github.com/SillyTavern/SillyTavern/issues?q=)?**
Yes/No

@@ -13,0 +13,0 @@

@@ -1,2 +0,2 @@

![image](https://github.com/Cohee1207/SillyTavern/assets/18619528/8c41a061-7f72-4d2b-9d54-e6d058209e7b)
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/8c41a061-7f72-4d2b-9d54-e6d058209e7b)

@@ -9,3 +9,3 @@ Mobile-friendly, Multi-API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI+proxies, Poe, WindowAI(Claude!)), VN-like Waifu Mode, Horde SD, System TTS, WorldInfo (lorebooks), customizable UI, auto-translate, and more prompt options than you'd ever want or need. Optional Extras server for more SD/TTS options + ChromaDB/Summarize.

NOTE: We have added [a FAQ](https://docs.sillytavern.app/usage/faq/) to answer most of your questions and help you get started.
NOTE: We have created a [Documentation website](https://docs.sillytavern.app/) to answer most of your questions and help you get started.

@@ -22,10 +22,10 @@ ### What is SillyTavern or TavernAI?

* main -🌟 **Recommended for most users.** This is the most stable and recommended branch, updated only when major releases are pushed. It's suitable for the majority of users.
* dev - ⚠️ **Not recommended for casual use.** This branch has the latest features, but be cautious as it may break at any time. Only for power users and enthusiasts.
* main -🌟 **Recommended for most users.** This is the most stable and recommended branch, updated only when major releases are pushed. It's suitable for the majority of users.
* dev - ⚠️ **Not recommended for casual use.** This branch has the latest features, but be cautious as it may break at any time. Only for power users and enthusiasts.
If you're not familiar with using the git CLI or don't understand what a branch is, don't worry! The main branch is always the preferable option for you.
If you're not familiar with using the git CLI or don't understand what a branch is, don't worry! The main branch is always the preferable option for you.
### What do I need other than Tavern?
On its own Tavern is useless, as it's just a user interface. You have to have access to an AI system backend that can act as the roleplay character. There are various supported backends: OpenAPI API (GPT), KoboldAI (either running locally or on Google Colab), and more. You can read more about this in [the FAQ](faq.md).
On its own Tavern is useless, as it's just a user interface. You have to have access to an AI system backend that can act as the roleplay character. There are various supported backends: OpenAPI API (GPT), KoboldAI (either running locally or on Google Colab), and more. You can read more about this in [the FAQ](https://docs.sillytavern.app/usage/faq/).

@@ -60,3 +60,3 @@ ### Do I need a powerful PC to run Tavern?

* Reddit: /u/RossAscends or /u/sillylossy
* [Post a GitHub issue](https://github.com/Cohee1207/SillyTavern/issues)
* [Post a GitHub issue](https://github.com/SillyTavern/SillyTavern/issues)

@@ -151,4 +151,4 @@ ## This version includes

* for Main Branch: `git clone https://github.com/Cohee1207/SillyTavern -b main`
* for Dev Branch: `git clone https://github.com/Cohee1207/SillyTavern -b dev`
* for Main Branch: `git clone https://github.com/SillyTavern/SillyTavern -b main`
* for Dev Branch: `git clone https://github.com/SillyTavern/SillyTavern -b dev`

@@ -161,3 +161,3 @@ 7. Once everything is cloned, double click `Start.bat` to make NodeJS install its requirements.

1. Install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended)
2. Download the zip from this GitHub repo. (Get the `Source code (zip)` from [Releases](https://github.com/Cohee1207/SillyTavern/releases/latest))
2. Download the zip from this GitHub repo. (Get the `Source code (zip)` from [Releases](https://github.com/SillyTavern/SillyTavern/releases/latest))
3. Unzip it into a folder of your choice

@@ -164,0 +164,0 @@ 4. Run `Start.bat` via double-clicking or in a command line.

@@ -20,2 +20,3 @@ {

"json5": "^2.2.3",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",

@@ -30,2 +31,3 @@ "multer": "^1.4.5-lts.1",

"png-chunks-extract": "^1.0.0",
"response-time": "^2.3.2",
"rimraf": "^3.0.2",

@@ -49,5 +51,5 @@ "sanitize-filename": "^1.6.3",

"type": "git",
"url": "https://github.com/Cohee1207/SillyTavern.git"
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.6.8",
"version": "1.7.0",
"scripts": {

@@ -73,3 +75,3 @@ "start": "node server.js",

"node_modules/**/*",
"poe_graphql/**/*"
"src/poe_graphql/**/*"
],

@@ -76,0 +78,0 @@ "outputPath": "dist",

@@ -1,2 +0,2 @@

const poe = require('./poe-client');
const poe = require('./src/poe-client');

@@ -6,3 +6,3 @@ async function test() {

await client.init('pb-cookie');
const bots = client.get_bot_names();

@@ -22,2 +22,2 @@ console.log(bots);

test();
test();

@@ -204,3 +204,3 @@ import {

const allowSelfResponses = false;
const favChecked = character.fav == 'true';
const favChecked = character.fav || character.fav == 'true';
const metadata = Object.assign({}, chat_metadata);

@@ -207,0 +207,0 @@ delete metadata.main_chat;

@@ -52,2 +52,4 @@ /* Polyfill indexOf. */

EventEmitter.prototype.emit = async function (event) {
console.debug('Event emitted: ' + event);
var i, listeners, length, args = [].slice.call(arguments, 1);

@@ -54,0 +56,0 @@

import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced } from "../script.js";
import { isSubsetOf } from "./utils.js";
import { isSubsetOf, debounce } from "./utils.js";
export {

@@ -8,5 +8,6 @@ getContext,

runGenerationInterceptors,
defaultRequestArgs,
doExtrasFetch,
modules,
extension_settings,
ModuleWorkerWrapper,
};

@@ -17,10 +18,39 @@

const defaultUrl = "http://localhost:5100";
export const saveMetadataDebounced = debounce(async () => await getContext().saveMetadata(), 1000);
// Disables parallel updates
class ModuleWorkerWrapper {
constructor(callback) {
this.isBusy = false;
this.callback = callback;
}
// Called by the extension
async update() {
// Don't touch me I'm busy...
if (this.isBusy) {
return;
}
// I'm free. Let's update!
try {
this.isBusy = true;
await this.callback();
}
finally {
this.isBusy = false;
}
}
}
const extension_settings = {
apiUrl: defaultUrl,
apiKey: '',
autoConnect: false,
disabledExtensions: [],
expressionOverrides: [],
memory: {},
note: {
default: '',
chara: [],
},

@@ -34,2 +64,4 @@ caption: {},

translate: {},
objective: {},
quickReply: {},
};

@@ -42,5 +74,41 @@

const getApiUrl = () => extension_settings.apiUrl;
const defaultRequestArgs = { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } };
let connectedToApi = false;
function showHideExtensionsMenu() {
// Get the number of menu items that are not hidden
const hasMenuItems = $('#extensionsMenu').children().filter((_, child) => $(child).css('display') !== 'none').length > 0;
// We have menu items, so we can stop checking
if (hasMenuItems) {
clearInterval(menuInterval);
}
// Show or hide the menu button
$('#extensionsMenuButton').toggle(hasMenuItems);
}
// Periodically check for new extensions
const menuInterval = setInterval(showHideExtensionsMenu, 1000);
async function doExtrasFetch(endpoint, args) {
if (!args) {
args = {}
}
if (!args.method) {
Object.assign(args, { method: 'GET' });
}
if (!args.headers) {
args.headers = {}
}
Object.assign(args.headers, {
'Authorization': `Bearer ${extension_settings.apiKey}`,
'Bypass-Tunnel-Reminder': 'bypass'
});
const response = await fetch(endpoint, args);
return response;
}
async function discoverExtensions() {

@@ -88,10 +156,19 @@ try {

const obj = {};
const promises = [];
for (const name of names) {
const response = await fetch(`/scripts/extensions/${name}/manifest.json`);
const promise = new Promise((resolve, reject) => {
fetch(`/scripts/extensions/${name}/manifest.json`).then(async response => {
if (response.ok) {
const json = await response.json();
obj[name] = json;
resolve();
}
}).catch(err => reject() && console.log('Could not load manifest.json for ' + name, err));
});
if (response.ok) {
const json = await response.json();
obj[name] = json;
}
promises.push(promise);
}
await Promise.allSettled(promises);
return obj;

@@ -102,2 +179,3 @@ }

const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
const promises = [];

@@ -120,5 +198,7 @@ for (let entry of extensions) {

if (!isDisabled) {
await addExtensionScript(name, manifest);
await addExtensionStyle(name, manifest);
activeExtensions.add(name);
const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]);
promise
.then(() => activeExtensions.add(name))
.catch(err => console.log('Could not activate extension: ' + name, err));
promises.push(promise);
}

@@ -140,2 +220,4 @@ else {

}
await Promise.allSettled(promises);
}

@@ -146,2 +228,4 @@

extension_settings.apiUrl = baseUrl;
const testApiKey = $("#extensions_api_key").val();
extension_settings.apiKey = testApiKey;
saveSettingsDebounced();

@@ -164,4 +248,4 @@ await connectToApi(baseUrl);

const buttonHTML =
`<div id="extensionsMenuButton" class="fa-solid fa-magic-wand-sparkles" title="Extras Extensions" /></div>`;
const extensionsMenuHTML = `<div id="extensionsMenu" class="list-group"></div>`;
`<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles" title="Extras Extensions" /></div>`;
const extensionsMenuHTML = `<div id="extensionsMenu" class="options-content" style="display: none;"></div>`;

@@ -174,3 +258,3 @@ $(document.body).append(extensionsMenuHTML);

const dropdown = $('#extensionsMenu');
dropdown.hide();
//dropdown.hide();

@@ -181,15 +265,31 @@ let popper = Popper.createPopper(button.get(0), dropdown.get(0), {

$(document).on('click touchend', function (e) {
$(button).on('click', function () {
popper.update()
dropdown.fadeIn(250);
});
$("html").on('touchstart mousedown', function (e) {
let clickTarget = $(e.target);
if (dropdown.is(':visible')
&& clickTarget.closest(button).length == 0
&& clickTarget.closest(dropdown).length == 0) {
$(dropdown).fadeOut(250);
}
});
}
/* $(document).on('click', function (e) {
const target = $(e.target);
if (target.is(dropdown)) return;
if (target.is(button) && !dropdown.is(":visible")) {
e.preventDefault();
dropdown.show(200);
if (target.is(button) && dropdown.is(':hidden')) {
dropdown.toggle(200);
popper.update();
} else {
}
if (target !== dropdown &&
target !== button &&
dropdown.is(":visible")) {
dropdown.hide(200);
}
});
}
} */

@@ -205,3 +305,3 @@ async function connectToApi(baseUrl) {

try {
const getExtensionsResult = await fetch(url, defaultRequestArgs);
const getExtensionsResult = await doExtrasFetch(url);

@@ -325,2 +425,3 @@ if (getExtensionsResult.ok) {

$("#extensions_url").val(extension_settings.apiUrl);
$("#extensions_api_key").val(extension_settings.apiKey);
$("#extensions_autoconnect").prop('checked', extension_settings.autoConnect);

@@ -343,3 +444,3 @@

await window[interceptorKey](chat);
} catch(e) {
} catch (e) {
console.error(`Failed running interceptor for ${manifest.display_name}`, e);

@@ -346,0 +447,0 @@ }

@@ -1,2 +0,5 @@

import { getContext } from "../../extensions.js";
import { generateQuietPrompt } from "../../../script.js";
import { getContext, saveMetadataDebounced } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { stringFormat } from "../../utils.js";
export { MODULE_NAME };

@@ -54,3 +57,3 @@

context.chatMetadata[METADATA_KEY] = file;
context.saveMetadata();
saveMetadataDebounced();
}

@@ -61,3 +64,3 @@

delete context.chatMetadata[METADATA_KEY];
context.saveMetadata();
saveMetadataDebounced();
}

@@ -92,2 +95,26 @@

const autoBgPrompt = `Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}`;
async function autoBackgroundCommand() {
const options = Array.from(document.querySelectorAll('.BGSampleTitle')).map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return;
}
const list = options.map(option => `- ${option.text}`).join('\n');
const prompt = stringFormat(autoBgPrompt, list);
const reply = await generateQuietPrompt(prompt);
const fuse = new Fuse(options, { keys: ['text'] });
const bestMatch = fuse.search(reply, { limit: 1 });
if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.');
return;
}
console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click();
}
$(document).ready(function () {

@@ -117,2 +144,12 @@ function addSettings() {

</div>
<div class="background_controls">
<div id="auto_background" class="menu_button">
<i class="fa-solid fa-wand-magic"></i>
Auto
</div>
<small>
Automatically select a background based on the chat context.<br>
Respects the "Lock" setting state.
</small>
</div>
<div>Preview</div>

@@ -129,2 +166,3 @@ <div id="custom_bg_preview">

$(document).on("click", ".bg_example", onSelectBackgroundClick);
$('#auto_background').on("click", autoBackgroundCommand);
}

@@ -134,2 +172,5 @@

setInterval(moduleWorker, UPDATE_INTERVAL);
});
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], " – locks a background for the currently selected chat", true, true);
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' – unlocks a background for the currently selected chat', true, true);
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' – automatically changes the background based on the chat context using the AI request prompt', true, true);
});

@@ -10,3 +10,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
}
"homePage": "https://github.com/SillyTavern/SillyTavern"
}
import { getBase64Async } from "../../utils.js";
import { getContext, getApiUrl } from "../../extensions.js";
import { getContext, getApiUrl, doExtrasFetch } from "../../extensions.js";
export { MODULE_NAME };

@@ -9,7 +9,3 @@

async function moduleWorker() {
const context = getContext();
context.onlineStatus === 'no_connection'
? $('#send_picture').hide(200)
: $('#send_picture').show(200);
$('#send_picture').toggle(getContext().onlineStatus !== 'no_connection');
}

@@ -71,3 +67,3 @@

const apiResult = await fetch(url, {
const apiResult = await doExtrasFetch(url, {
method: 'POST',

@@ -124,2 +120,2 @@ headers: {

setInterval(moduleWorker, UPDATE_INTERVAL);
});
});

@@ -12,3 +12,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
}
"homePage": "https://github.com/SillyTavern/SillyTavern"
}
import { callPopup } from "../../../script.js";
import { getContext } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
export { MODULE_NAME };

@@ -8,15 +9,13 @@

function setDiceIcon() {
const sendButton = document.getElementById('roll_dice');
/* sendButton.style.backgroundImage = `url(/img/dice-solid.svg)`; */
//sendButton.classList.remove('spin');
}
async function doDiceRoll(customDiceFormula) {
let value = typeof customDiceFormula === 'string' ? customDiceFormula.trim() : $(this).data('value');
async function doDiceRoll() {
let value = $(this).data('value');
if (value == 'custom') {
value = await callPopup('Enter the dice formula:<br><i>(for example, <tt>2d6</tt>)</i>', 'input');
value = await callPopup('Enter the dice formula:<br><i>(for example, <tt>2d6</tt>)</i>', 'input');x
}
if (!value) {
return;
}
const isValid = droll.validate(value);

@@ -28,2 +27,4 @@

context.sendSystemMessage('generic', `${context.name1} rolls a ${value}. The result is: ${result.total} (${result.rolls})`, { isSmallSys: true });
} else {
toastr.warning('Invalid dice formula');
}

@@ -34,3 +35,3 @@ }

const buttonHtml = `
<div id="roll_dice" class="list-group-item flex-container flexGap5">
<div id="roll_dice" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-dice extensionsMenuExtensionButton" title="Roll Dice" /></div>

@@ -64,3 +65,3 @@ Roll Dice

let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
placement: 'bottom',
placement: 'top',
});

@@ -74,6 +75,6 @@

dropdown.show(200);
dropdown.fadeIn(250);
popper.update();
} else {
dropdown.hide(200);
dropdown.fadeOut(250);
}

@@ -92,15 +93,11 @@ });

async function moduleWorker() {
const context = getContext();
context.onlineStatus === 'no_connection'
? $('#roll_dice').hide(200)
: $('#roll_dice').show(200);
$('#roll_dice').toggle(getContext().onlineStatus !== 'no_connection');
}
$(document).ready(function () {
jQuery(function () {
addDiceScript();
addDiceRollButton();
setDiceIcon();
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
});
registerSlashCommand('roll', (_, value) => doDiceRoll(value), ['r'], "<span class='monospace'>(dice formula)</span> – roll the dice. For example, /roll 2d6", false, true);
});

@@ -10,3 +10,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
}
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

@@ -1,3 +0,6 @@

import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js";
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { dragElement, isMobile } from "../../RossAscends-mods.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
import { power_user } from "../../power-user.js";
import { onlyUnique, debounce, getCharaFilename } from "../../utils.js";
export { MODULE_NAME };

@@ -7,2 +10,3 @@

const UPDATE_INTERVAL = 2000;
const FALLBACK_EXPRESSION = 'joy';
const DEFAULT_EXPRESSIONS = [

@@ -45,2 +49,252 @@ "admiration",

function isVisualNovelMode() {
return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
}
async function forceUpdateVisualNovelMode() {
if (isVisualNovelMode()) {
await updateVisualNovelMode();
}
}
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, 100);
async function updateVisualNovelMode(name, expression) {
const container = $('#visual-novel-wrapper');
await visualNovelRemoveInactive(container);
const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression);
// calculate layer indices based on recent messages
await visualNovelUpdateLayers(container);
await Promise.allSettled(setSpritePromises);
// update again based on new sprites
if (setSpritePromises.length > 0) {
await visualNovelUpdateLayers(container);
}
}
async function visualNovelRemoveInactive(container) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const removeInactiveCharactersPromises = [];
// remove inactive characters after 1 second
container.find('.expression-holder').each((_, current) => {
const promise = new Promise(resolve => {
const element = $(current);
const avatar = element.data('avatar');
if (!group.members.includes(avatar) || group.disabled_members.includes(avatar)) {
element.fadeOut(250, () => {
element.remove();
resolve();
});
} else {
resolve();
}
});
removeInactiveCharactersPromises.push(promise);
});
await Promise.allSettled(removeInactiveCharactersPromises);
}
async function visualNovelSetCharacterSprites(container, name, expression) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const labels = await getExpressionsList();
const createCharacterPromises = [];
const setSpritePromises = [];
for (const avatar of group.members) {
const isDisabled = group.disabled_members.includes(avatar);
// skip disabled characters
if (isDisabled) {
continue;
}
const character = context.characters.find(x => x.avatar == avatar);
if (!character) {
continue;
}
let spriteFolderName = character.name;
const avatarFileName = getSpriteFolderName({ original_avatar: character.avatar });
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
spriteFolderName = expressionOverride.path;
}
// download images if not downloaded yet
if (spriteCache[spriteFolderName] === undefined) {
spriteCache[spriteFolderName] = await getSpritesList(spriteFolderName);
}
const sprites = spriteCache[spriteFolderName];
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
const defaultSpritePath = sprites.find(x => x.label === FALLBACK_EXPRESSION)?.path;
const noSprites = sprites.length === 0;
if (expressionImage.length > 0) {
if (name == spriteFolderName) {
await validateImages(spriteFolderName, true);
setExpressionOverrideHtml(true); // <= force clear expression override input
const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : '';
const path = currentSpritePath || defaultSpritePath || '';
const img = expressionImage.find('img');
setImage(img, path);
}
expressionImage.toggleClass('hidden', noSprites);
} else {
const template = $('#expression-holder').clone();
template.attr('id', `expression-${avatar}`);
template.attr('data-avatar', avatar);
template.find('.drag-grabber').attr('id', `expression-${avatar}header`);
$('#visual-novel-wrapper').append(template);
dragElement(template[0]);
template.toggleClass('hidden', noSprites);
setImage(template.find('img'), defaultSpritePath || '');
const fadeInPromise = new Promise(resolve => {
template.fadeIn(250, () => resolve());
});
createCharacterPromises.push(fadeInPromise);
const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
setSpritePromises.push(setSpritePromise);
}
}
await Promise.allSettled(createCharacterPromises);
return setSpritePromises;
}
async function visualNovelUpdateLayers(container) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const recentMessages = context.chat.map(x => x.original_avatar).filter(x => x).reverse().filter(onlyUnique);
const filteredMembers = group.members.filter(x => !group.disabled_members.includes(x));
const layerIndices = filteredMembers.slice().sort((a, b) => {
const aRecentIndex = recentMessages.indexOf(a);
const bRecentIndex = recentMessages.indexOf(b);
const aFilteredIndex = filteredMembers.indexOf(a);
const bFilteredIndex = filteredMembers.indexOf(b);
if (aRecentIndex !== -1 && bRecentIndex !== -1) {
return bRecentIndex - aRecentIndex;
} else if (aRecentIndex !== -1) {
return 1;
} else if (bRecentIndex !== -1) {
return -1;
} else {
return aFilteredIndex - bFilteredIndex;
}
});
const setLayerIndicesPromises = [];
const sortFunction = (a, b) => {
const avatarA = $(a).data('avatar');
const avatarB = $(b).data('avatar');
const indexA = filteredMembers.indexOf(avatarA);
const indexB = filteredMembers.indexOf(avatarB);
return indexA - indexB;
};
const containerWidth = container.width();
const pivotalPoint = containerWidth * 0.5;
let images = $('.expression-holder');
let imagesWidth = [];
images.sort(sortFunction).each(function () {
imagesWidth.push($(this).width());
});
let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
let currentPosition = pivotalPoint - (totalWidth / 2);
if (totalWidth > containerWidth) {
let totalOverlap = totalWidth - containerWidth;
let totalWidthWithoutWidest = imagesWidth.reduce((a, b) => a + b, 0) - Math.max(...imagesWidth);
let overlaps = imagesWidth.map(width => (width / totalWidthWithoutWidest) * totalOverlap);
imagesWidth = imagesWidth.map((width, index) => width - overlaps[index]);
currentPosition = 0; // Reset the initial position to 0
}
images.sort(sortFunction).each((index, current) => {
const element = $(current);
// skip repositioning of dragged elements
if (element.data('dragged')) {
currentPosition += imagesWidth[index];
return;
}
const avatar = element.data('avatar');
const layerIndex = layerIndices.indexOf(avatar);
element.css('z-index', layerIndex);
element.show();
const promise = new Promise(resolve => {
element.animate({ left: currentPosition + 'px' }, 500, () => {
resolve();
});
});
currentPosition += imagesWidth[index];
setLayerIndicesPromises.push(promise);
});
await Promise.allSettled(setLayerIndicesPromises);
}
async function setLastMessageSprite(img, avatar, labels) {
const context = getContext();
const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
if (lastMessage) {
const text = lastMessage.mes || '';
let spriteFolderName = lastMessage.name;
const avatarFileName = getSpriteFolderName(lastMessage);
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
spriteFolderName = expressionOverride.path;
}
const sprites = spriteCache[spriteFolderName] || [];
const label = await getExpressionLabel(text);
const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
if (path) {
setImage(img, path);
}
}
}
function setImage(img, path) {
img.attr('src', path);
img.removeClass('default');
img.off('error');
img.on('error', function () {
console.debug('Error loading image', path);
$(this).off('error');
$(this).attr('src', '');
});
}
function onExpressionsShowDefaultInput() {

@@ -63,20 +317,2 @@ const value = $(this).prop('checked');

let isWorkerBusy = false;
async function moduleWorkerWrapper() {
// Don't touch me I'm busy...
if (isWorkerBusy) {
return;
}
// I'm free. Let's update!
try {
isWorkerBusy = true;
await moduleWorker();
}
finally {
isWorkerBusy = false;
}
}
async function moduleWorker() {

@@ -97,7 +333,35 @@ const context = getContext();

const vnMode = isVisualNovelMode();
const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible');
if (vnMode) {
$('#expression-wrapper').hide();
$('#visual-novel-wrapper').show();
} else {
$('#expression-wrapper').show();
$('#visual-novel-wrapper').hide();
}
const vnStateChanged = vnMode !== vnWrapperVisible;
if (vnStateChanged) {
lastMessage = null;
$('#visual-novel-wrapper').empty();
$("#expression-holder").css({ top: '', left: '', right: '', bottom: '', height: '', width: '', margin: '' });
}
const currentLastMessage = getLastCharacterMessage();
let spriteFolderName = currentLastMessage.name;
const avatarFileName = getSpriteFolderName(currentLastMessage);
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
spriteFolderName = expressionOverride.path;
}
// character has no expressions or it is not loaded
if (Object.keys(spriteCache).length === 0) {
await validateImages(currentLastMessage.name);
await validateImages(spriteFolderName);
lastCharacter = context.groupId || context.characterId;

@@ -113,3 +377,4 @@ }

if (context.groupId) {
await validateImages(currentLastMessage.name, true);
await validateImages(spriteFolderName, true);
await forceUpdateVisualNovelMode();
}

@@ -125,3 +390,4 @@

expressionsList = await getExpressionsList();
await validateImages(currentLastMessage.name, true);
await validateImages(spriteFolderName, true);
await forceUpdateVisualNovelMode();
}

@@ -132,3 +398,2 @@

// check if last message changed

@@ -147,28 +412,18 @@ if ((lastCharacter === context.characterId || lastCharacter === context.groupId)

inApiCall = true;
const url = new URL(getApiUrl());
url.pathname = '/api/classify';
let expression = await getExpressionLabel(currentLastMessage.mes);
const apiResult = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: currentLastMessage.mes })
});
// If we're not already overriding the folder name, account for group chats.
if (spriteFolderName === currentLastMessage.name && !context.groupId) {
spriteFolderName = context.name2;
}
if (apiResult.ok) {
const name = context.groupId ? currentLastMessage.name : context.name2;
const force = !!context.groupId;
const data = await apiResult.json();
let expression = data.classification[0].label;
const force = !!context.groupId;
// Character won't be angry on you for swiping
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
expression = 'joy';
}
setExpression(name, expression, force);
// Character won't be angry on you for swiping
if (currentLastMessage.mes == '...' && expressionsList.includes(FALLBACK_EXPRESSION)) {
expression = FALLBACK_EXPRESSION;
}
await sendExpressionCall(spriteFolderName, expression, force, vnMode);
}

@@ -185,2 +440,57 @@ catch (error) {

function getSpriteFolderName(message) {
const context = getContext();
let avatarPath = '';
if (context.groupId) {
avatarPath = message.original_avatar || context.characters.find(x => message.force_avatar && message.force_avatar.includes(encodeURIComponent(x.avatar)))?.avatar;
}
else if (context.characterId) {
avatarPath = getCharaFilename();
}
if (!avatarPath) {
return '';
}
const folderName = avatarPath.replace(/\.[^/.]+$/, "");
return folderName;
}
async function sendExpressionCall(name, expression, force, vnMode) {
if (!vnMode) {
vnMode = isVisualNovelMode();
}
if (vnMode) {
await updateVisualNovelMode(name, expression);
} else {
setExpression(name, expression, force);
}
}
async function getExpressionLabel(text) {
// Return if text is undefined, saving a costly fetch request
if (!modules.includes('classify') || !text) {
return FALLBACK_EXPRESSION;
}
const url = new URL(getApiUrl());
url.pathname = '/api/classify';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: text }),
});
if (apiResult.ok) {
const data = await apiResult.json();
return data.classification[0].label;
}
}
function getLastCharacterMessage() {

@@ -195,6 +505,6 @@ const context = getContext();

return { mes: mes.mes, name: mes.name };
return { mes: mes.mes, name: mes.name, original_avatar: mes.original_avatar, force_avatar: mes.force_avatar };
}
return { mes: '', name: null };
return { mes: '', name: null, original_avatar: null, force_avatar: null };
}

@@ -219,3 +529,3 @@

if (forceRedrawCached && $('#image_list').data('name') !== character) {
console.log('force redrawing character sprites list')
console.debug('force redrawing character sprites list')
drawSpritesList(character, labels, spriteCache[character]);

@@ -269,3 +579,3 @@ }

async function getSpritesList(name) {
console.log('getting sprites list');
console.debug('getting sprites list');

@@ -298,3 +608,3 @@ try {

try {
const apiResult = await fetch(url, {
const apiResult = await doExtrasFetch(url, {
method: 'GET',

@@ -318,3 +628,3 @@ headers: { 'Bypass-Tunnel-Reminder': 'bypass' },

async function setExpression(character, expression, force) {
console.log('entered setExpressions');
console.debug('entered setExpressions');
await validateImages(character);

@@ -324,5 +634,24 @@ const img = $('img.expression');

const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
console.log('checking for expression images to show..');
console.debug('checking for expression images to show..');
if (sprite) {
console.log('setting expression from character images folder');
console.debug('setting expression from character images folder');
if (force && isVisualNovelMode()) {
const context = getContext();
const group = context.groups.find(x => x.id === context.groupId);
for (const member of group.members) {
const groupMember = context.characters.find(x => x.avatar === member);
if (!groupMember) {
continue;
}
if (groupMember.name == character) {
setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);
return;
}
}
}
img.attr('src', sprite.path);

@@ -332,3 +661,5 @@ img.removeClass('default');

img.on('error', function () {
console.debug('Expression image error', sprite.path);
$(this).attr('src', '');
$(this).off('error');
if (force && extension_settings.expressions.showDefault) {

@@ -345,3 +676,3 @@ setDefault();

function setDefault() {
console.log('setting default');
console.debug('setting default');
const defImgUrl = `/img/default-expressions/${expression}.png`;

@@ -422,2 +753,82 @@ //console.log(defImgUrl);

async function onClickExpressionOverrideButton() {
const context = getContext();
const currentLastMessage = getLastCharacterMessage();
const avatarFileName = getSpriteFolderName(currentLastMessage);
// If the avatar name couldn't be found, abort.
if (!avatarFileName) {
console.debug(`Could not find filename for character with name ${currentLastMessage.name} and ID ${context.characterId}`);
return;
}
const overridePath = $("#expression_override").val();
const existingOverrideIndex = extension_settings.expressionOverrides.findIndex((e) =>
e.name == avatarFileName
);
// If the path is empty, delete the entry from overrides
if (overridePath === undefined || overridePath.length === 0) {
if (existingOverrideIndex === -1) {
return;
}
extension_settings.expressionOverrides.splice(existingOverrideIndex, 1);
console.debug(`Removed existing override for ${avatarFileName}`);
} else {
// Properly override objects and clear the sprite cache of the previously set names
const existingOverride = extension_settings.expressionOverrides[existingOverrideIndex];
if (existingOverride) {
Object.assign(existingOverride, { path: overridePath });
delete spriteCache[existingOverride.name];
} else {
const characterOverride = { name: avatarFileName, path: overridePath };
extension_settings.expressionOverrides.push(characterOverride);
delete spriteCache[currentLastMessage.name];
}
console.debug(`Added/edited expression override for character with filename ${avatarFileName} to folder ${overridePath}`);
}
saveSettingsDebounced();
// Refresh sprites list. Assume the override path has been properly handled.
try {
$('#visual-novel-wrapper').empty();
await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true);
const expression = await getExpressionLabel(currentLastMessage.mes);
await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, true);
forceUpdateVisualNovelMode();
} catch (error) {
console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`);
}
}
async function onClickExpressionOverrideRemoveAllButton() {
// Remove all the overrided entries from sprite cache
for (const element of extension_settings.expressionOverrides) {
delete spriteCache[element.name];
}
extension_settings.expressionOverrides = [];
saveSettingsDebounced();
console.debug("All expression image overrides have been cleared.");
// Refresh sprites list to use the default name if applicable
try {
$('#visual-novel-wrapper').empty();
const currentLastMessage = getLastCharacterMessage();
await validateImages(currentLastMessage.name, true);
const expression = await getExpressionLabel(currentLastMessage.mes);
await sendExpressionCall(currentLastMessage.name, expression, true);
forceUpdateVisualNovelMode();
console.debug(extension_settings.expressionOverrides);
} catch (error) {
console.debug(`The current expression could not be set because of error: ${error}`);
}
}
async function onClickExpressionUploadPackButton() {

@@ -478,2 +889,24 @@ const name = $('#image_list').data('name');

function setExpressionOverrideHtml(forceClear = false) {
const currentLastMessage = getLastCharacterMessage();
const avatarFileName = getSpriteFolderName(currentLastMessage);
if (!avatarFileName) {
return;
}
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
$("#expression_override").val(expressionOverride.path);
} else if (expressionOverride) {
delete extension_settings.expressionOverrides[expressionOverride.name];
}
if (forceClear && !expressionOverride) {
$("#expression_override").val("");
}
}
(function () {

@@ -490,2 +923,10 @@ function addExpressionImage() {

}
function addVisualNovelMode() {
const html = `
<div id="visual-novel-wrapper">
</div>`
const element = $(html);
element.hide();
$('body').append(element);
}
function addSettings() {

@@ -497,9 +938,15 @@

<div class="inline-drawer-toggle inline-drawer-header">
<b>Expression images</b>
<b>Character Expressions</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<p class="offline_mode">You are in offline mode. Click on the image below to set the expression.</p>
<div class="offline_mode">
<small>You are in offline mode. Click on the image below to set the expression.</small>
</div>
<div class="flex-container flexnowrap">
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
</div>
<div id="image_list"></div>
<div class="expression_buttons">
<div class="expression_buttons flex-container spaceEvenly">
<div id="expression_upload_pack_button" class="menu_button">

@@ -509,2 +956,6 @@ <i class="fa-solid fa-file-zipper"></i>

</div>
<div id="expression_override_cleanup_button" class="menu_button">
<i class="fa-solid fa-trash-can"></i>
<span>Remove all image overrides</span>
</div>
</div>

@@ -523,8 +974,11 @@ <p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.

$('#extensions_settings').append(html);
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
$('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);
$('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
$('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);
$(document).on('click', '.expression_list_item', onClickExpressionImage);
$(document).on('click', '.expression_list_upload', onClickExpressionUpload);
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
$(window).on("resize", updateVisualNovelModeDebounced);
$('.expression_settings').hide();

@@ -534,5 +988,17 @@ }

addExpressionImage();
addVisualNovelMode();
addSettings();
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL);
moduleWorkerWrapper();
const wrapper = new ModuleWorkerWrapper(moduleWorker);
const updateFunction = wrapper.update.bind(wrapper);
setInterval(updateFunction, UPDATE_INTERVAL);
moduleWorker();
eventSource.on(event_types.CHAT_CHANGED, () => {
setExpressionOverrideHtml();
if (isVisualNovelMode()) {
$('#visual-novel-wrapper').empty();
}
});
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
})();

@@ -12,3 +12,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
}
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

@@ -1,9 +0,15 @@

import { chat_metadata, saveSettingsDebounced } from "../../../script.js";
import { extension_settings, getContext } from "../../extensions.js";
import {
chat_metadata,
eventSource,
event_types,
getTokenCount,
saveSettingsDebounced,
this_chid,
} from "../../../script.js";
import { selected_group } from "../../group-chats.js";
import { ModuleWorkerWrapper, extension_settings, getContext, saveMetadataDebounced } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { debounce } from "../../utils.js";
import { getCharaFilename, debounce } from "../../utils.js";
export { MODULE_NAME };
const saveMetadataDebounced = debounce(async () => await getContext().saveMetadata(), 1000);
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory

@@ -69,4 +75,9 @@ const UPDATE_INTERVAL = 1000;

const setMainPromptTokenCounterDebounced = debounce((value) => $('#extension_floating_prompt_token_counter').text(getTokenCount(value)), 1000);
const setCharaPromptTokenCounterDebounced = debounce((value) => $('#extension_floating_chara_token_counter').text(getTokenCount(value)), 1000);
const setDefaultPromptTokenCounterDebounced = debounce((value) => $('#extension_floating_default_token_counter').text(getTokenCount(value)), 1000);
async function onExtensionFloatingPromptInput() {
chat_metadata[metadata_keys.prompt] = $(this).val();
setMainPromptTokenCounterDebounced(chat_metadata[metadata_keys.prompt]);
saveMetadataDebounced();

@@ -97,4 +108,62 @@ }

function onExtensionFloatingCharaPromptInput() {
const tempPrompt = $(this).val();
const avatarName = getCharaFilename();
let tempCharaNote = {
name: avatarName,
prompt: tempPrompt
}
setCharaPromptTokenCounterDebounced(tempPrompt);
let existingCharaNoteIndex;
let existingCharaNote;
if (extension_settings.note.chara) {
existingCharaNoteIndex = extension_settings.note.chara.findIndex((e) => e.name === avatarName);
existingCharaNote = extension_settings.note.chara[existingCharaNoteIndex]
}
if (tempPrompt.length === 0 &&
extension_settings.note.chara &&
existingCharaNote &&
!existingCharaNote.useChara
) {
extension_settings.note.chara.splice(existingCharaNoteIndex, 1);
}
else if (extension_settings.note.chara && existingCharaNote) {
Object.assign(existingCharaNote, tempCharaNote);
}
else if (avatarName && tempPrompt.length > 0) {
if (!extension_settings.note.chara) {
extension_settings.note.chara = []
}
Object.assign(tempCharaNote, { useChara: false })
extension_settings.note.chara.push(tempCharaNote);
} else {
console.log("Character author's note error: No avatar name key could be found.");
toastr.error("Something went wrong. Could not save character's author's note.");
// Don't save settings if something went wrong
return;
}
saveSettingsDebounced();
}
function onExtensionFloatingCharaCheckboxChanged() {
const value = !!$(this).prop('checked');
const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
if (charaNote) {
charaNote.useChara = value;
saveSettingsDebounced();
}
}
function onExtensionFloatingDefaultInput() {
extension_settings.note.default = $(this).val();
setDefaultPromptTokenCounterDebounced(extension_settings.note.default);
saveSettingsDebounced();

@@ -112,21 +181,11 @@ }

$(`input[name="extension_floating_position"][value="${chat_metadata[metadata_keys.position]}"]`).prop('checked', true);
$('#extension_floating_default').val(extension_settings.note.default);
}
let isWorkerBusy = false;
if (extension_settings.note.chara) {
const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
async function moduleWorkerWrapper() {
// Don't touch me I'm busy...
if (isWorkerBusy) {
return;
$('#extension_floating_chara').val(charaNote ? charaNote.prompt : '');
$('#extension_use_floating_chara').prop('checked', charaNote ? charaNote.useChara : false);
}
// I'm free. Let's update!
try {
isWorkerBusy = true;
await moduleWorker();
}
finally {
isWorkerBusy = false;
}
$('#extension_floating_default').val(extension_settings.note.default);
}

@@ -146,4 +205,4 @@

// special case for new chat
if (Array.isArray(context.chat) && context.chat.length === 1) {
// interval 1 should be inserted no matter what
if (chat_metadata[metadata_keys.interval] === 1) {
lastMessageNumber = 1;

@@ -162,3 +221,13 @@ }

const shouldAddPrompt = messagesTillInsertion == 0;
const prompt = shouldAddPrompt ? $('#extension_floating_prompt').val() : '';
let prompt = shouldAddPrompt ? $('#extension_floating_prompt').val() : '';
if (shouldAddPrompt && extension_settings.note.chara) {
const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
// Only replace with the chara note if the user checked the box
if (charaNote && charaNote.useChara) {
prompt = charaNote.prompt;
}
}
context.setExtensionPrompt(MODULE_NAME, prompt, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);

@@ -168,2 +237,56 @@ $('#extension_floating_counter').text(shouldAddPrompt ? '0' : messagesTillInsertion);

function onANMenuItemClick() {
if (selected_group || this_chid) {
if ($("#floatingPrompt").css("display") !== 'flex') {
$("#floatingPrompt").css("display", "flex");
$("#floatingPrompt").css("opacity", 0.0);
$("#floatingPrompt").transition({
opacity: 1.0,
duration: 250,
});
if ($("#ANBlockToggle")
.siblings('.inline-drawer-content')
.css('display') !== 'block') {
$("#ANBlockToggle").click();
}
} else {
$("#floatingPrompt").transition({
opacity: 0.0,
duration: 250,
});
setTimeout(function () {
$("#floatingPrompt").hide();
}, 250);
}
//duplicate options menu close handler from script.js
//because this listener takes priority
$("#options").stop().fadeOut(250);
} else {
toastr.warning(`Select a character before trying to use Author's Note`, '', { timeOut: 2000 });
}
}
function onChatChanged() {
const tokenCounter1 = chat_metadata[metadata_keys.prompt] ? getTokenCount(chat_metadata[metadata_keys.prompt]) : 0;
$('#extension_floating_prompt_token_counter').text(tokenCounter1);
let tokenCounter2;
if (extension_settings.note.chara) {
const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
if (charaNote) {
tokenCounter2 = getTokenCount(charaNote.prompt);
}
}
if (tokenCounter2) {
$('#extension_floating_chara_token_counter').text(tokenCounter2);
}
const tokenCounter3 = extension_settings.note.default ? getTokenCount(extension_settings.note.default) : 0;
$('#extension_floating_default_token_counter').text(tokenCounter3);
}
(function () {

@@ -173,3 +296,6 @@ function addExtensionsSettings() {

<div id="floatingPrompt" class="drawer-content flexGap5">
<div id="floatingPromptheader" class="fa-solid fa-grip drag-grabber"></div>
<div class="panelControlBar flex-container">
<div id="floatingPromptheader" class="fa-solid fa-grip drag-grabber"></div>
<div id="ANClose" class="fa-solid fa-circle-xmark"></div>
</div>
<div name="floatingPromptHolder">

@@ -180,2 +306,3 @@ <div class="inline-drawer">

<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>

@@ -187,5 +314,6 @@ <div class="inline-drawer-content">

</small>
<textarea id="extension_floating_prompt" class="text_pole" rows="8" maxlength="10000"></textarea>
<div class="extension_token_counter">Tokens: <span id="extension_floating_prompt_token_counter">0</small></div>
<div class="floating_prompt_radio_group">

@@ -202,6 +330,6 @@ <label>

<!--<label for="extension_floating_interval">In-Chat Insertion Depth</label>-->
<label for="extension_floating_interval">Insertion Frequency</label>
<input id="extension_floating_interval" class="text_pole widthUnset" type="number" min="0" max="999" /><small> (0 = Disable)</small>
<label for="extension_floating_interval">Insertion Frequency</label>
<input id="extension_floating_interval" class="text_pole widthUnset" type="number" min="0" max="999" /><small> (0 = Disable, 1 = Always)</small>
<br>

@@ -215,2 +343,21 @@

<div class="inline-drawer">
<div id="charaANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Character Author's Note</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small>Will be automatically added as the author's note for this character.</small>
<textarea id="extension_floating_chara" class="text_pole" rows="8" maxlength="10000"
placeholder="Example:\n[Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea>
<div class="extension_token_counter">Tokens: <span id="extension_floating_chara_token_counter">0</small></div>
<label for="extension_use_floating_chara">
<input id="extension_use_floating_chara" type="checkbox" />
<span data-i18n="Use character author's note">Use character author's note</span>
</label>
</div>
</div>
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">

@@ -222,5 +369,6 @@ <b>Default Author's Note</b>

<small>Will be automatically added as the Author's Note for all new chats.</small>
<textarea id="extension_floating_default" class="text_pole" rows="8" maxlength="10000"
placeholder="Example:\n[Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea>
<div class="extension_token_counter">Tokens: <span id="extension_floating_default_token_counter">0</small></div>
</div>

@@ -232,2 +380,9 @@ </div>

const ANButtonHtml = `
<a id="option_toggle_AN">
<i class="fa-lg fa-solid fa-note-sticky"></i>
<span data-i18n="Author's Note">Author's Note</span>
</a>
`;
$('#options .options-content').prepend(ANButtonHtml);
$('#movingDivs').append(settingsHtml);

@@ -237,8 +392,20 @@ $('#extension_floating_prompt').on('input', onExtensionFloatingPromptInput);

$('#extension_floating_depth').on('input', onExtensionFloatingDepthInput);
$('#extension_floating_chara').on('input', onExtensionFloatingCharaPromptInput);
$('#extension_use_floating_chara').on('input', onExtensionFloatingCharaCheckboxChanged);
$('#extension_floating_default').on('input', onExtensionFloatingDefaultInput);
$('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput);
$('#ANClose').on('click', function () {
$("#floatingPrompt").transition({
opacity: 0,
duration: 200,
easing: 'ease-in-out',
});
setTimeout(function () { $('#floatingPrompt').hide() }, 200);
})
$("#option_toggle_AN").on('click', onANMenuItemClick);
}
addExtensionsSettings();
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL);
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
registerSlashCommand('note', setNoteTextCommand, [], "<span class='monospace'>(text)</span> – sets an author's note for the currently selected chat", true, true);

@@ -248,2 +415,3 @@ registerSlashCommand('depth', setNoteDepthCommand, [], "<span class='monospace'>(number)</span> – sets an author's note depth for in-chat positioning", true, true);

registerSlashCommand('pos', setNotePositionCommand, ['position'], "(<span class='monospace'>chat</span> or <span class='monospace'>scenario</span>) – sets an author's note position", true, true);
})();
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
})();

@@ -10,3 +10,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
}
"homePage": "https://github.com/SillyTavern/SillyTavern"
}
import { saveSettingsDebounced, getCurrentChatId, system_message_types, eventSource, event_types } from "../../../script.js";
import { humanizedDateTime } from "../../RossAscends-mods.js";
import { getApiUrl, extension_settings, getContext } from "../../extensions.js";
import { getFileText, onlyUnique, splitRecursive } from "../../utils.js";
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js";
import { getFileText, onlyUnique, splitRecursive, IndexedDBStore } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'chromadb';
const dbStore = new IndexedDBStore('SillyTavern', MODULE_NAME);

@@ -14,3 +15,3 @@ const defaultSettings = {

keep_context_min: 1,
keep_context_max: 100,
keep_context_max: 500,
keep_context_step: 1,

@@ -20,3 +21,3 @@

n_results_min: 0,
n_results_max: 100,
n_results_max: 500,
n_results_step: 1,

@@ -40,11 +41,10 @@

const chatStateFlags = {};
function invalidateMessageSyncState(messageId) {
async function invalidateMessageSyncState(messageId) {
console.log('CHROMADB: invalidating message sync state', messageId);
const state = getChatSyncState();
state[messageId] = false;
const state = await getChatSyncState();
state[messageId] = 0;
await dbStore.put(getCurrentChatId(), state);
}
function getChatSyncState() {
async function getChatSyncState() {
const currentChatId = getCurrentChatId();

@@ -56,3 +56,3 @@ if (!checkChatId(currentChatId)) {

const context = getContext();
const chatState = chatStateFlags[currentChatId] || [];
const chatState = (await dbStore.get(currentChatId)) || [];

@@ -77,6 +77,6 @@ // if the chat length has decreased, it means that some messages were deleted

if (chatState[i] === undefined) {
chatState[i] = false;
chatState[i] = 0;
}
}
chatStateFlags[currentChatId] = chatState;
await dbStore.put(currentChatId, chatState);

@@ -91,3 +91,3 @@ return chatState;

console.log(`loading chromadb strat:${extension_settings.chromadb.strategy}`);
console.debug(`loading chromadb strat:${extension_settings.chromadb.strategy}`);
$("#chromadb_strategy option[value=" + extension_settings.chromadb.strategy + "]").attr(

@@ -105,3 +105,3 @@ "selected",

function onStrategyChange() {
console.log('changing chromadb strat');
console.debug('changing chromadb strat');
extension_settings.chromadb.strategy = $('#chromadb_strategy').val();

@@ -154,3 +154,3 @@

const messagesDeepCopy = JSON.parse(JSON.stringify(messages));
let splittedMessages = [];
let splitMessages = [];

@@ -160,3 +160,3 @@ let id = 0;

const split = splitRecursive(m.mes, extension_settings.chromadb.split_length);
splittedMessages.push(...split.map(text => ({
splitMessages.push(...split.map(text => ({
...m,

@@ -171,10 +171,10 @@ mes: text,

splittedMessages = filterSyncedMessages(splittedMessages);
splitMessages = await filterSyncedMessages(splitMessages);
// no messages to add
if (splittedMessages.length === 0) {
if (splitMessages.length === 0) {
return { count: 0 };
}
const transformedMessages = splittedMessages.map((m) => ({
const transformedMessages = splitMessages.map((m) => ({
id: m.id,

@@ -187,3 +187,3 @@ role: m.is_user ? 'user' : 'assistant',

const addMessagesResult = await fetch(url, {
const addMessagesResult = await doExtrasFetch(url, {
method: 'POST',

@@ -196,3 +196,2 @@ headers: postHeaders,

const addMessagesData = await addMessagesResult.json();
return addMessagesData; // { count: 1 }

@@ -204,8 +203,8 @@ }

function filterSyncedMessages(splittedMessages) {
const syncState = getChatSyncState();
async function filterSyncedMessages(splitMessages) {
const syncState = await getChatSyncState();
const removeIndices = [];
const syncedIndices = [];
for (let i = 0; i < splittedMessages.length; i++) {
const index = splittedMessages[i].index;
for (let i = 0; i < splitMessages.length; i++) {
const index = splitMessages[i].index;

@@ -221,17 +220,12 @@ if (syncState[index]) {

for (const index of syncedIndices) {
syncState[index] = true;
syncState[index] = 1;
}
logSyncState(syncState);
console.debug('CHROMADB: sync state', syncState.map((v, i) => ({ id: i, synced: v })));
await dbStore.put(getCurrentChatId(), syncState);
// remove messages that are already synced
return splittedMessages.filter((_, i) => !removeIndices.includes(i));
return splitMessages.filter((_, i) => !removeIndices.includes(i));
}
function logSyncState(syncState) {
const chat = getContext().chat;
console.log('CHROMADB: sync state');
console.table(syncState.map((v, i) => ({ synced: v, name: chat[i].name, message: chat[i].mes })));
}
async function onPurgeClick() {

@@ -245,3 +239,3 @@ const chat_id = getCurrentChatId();

const purgeResult = await fetch(url, {
const purgeResult = await doExtrasFetch(url, {
method: 'POST',

@@ -253,3 +247,3 @@ headers: postHeaders,

if (purgeResult.ok) {
delete chatStateFlags[chat_id];
await dbStore.delete(chat_id);
toastr.success('ChromaDB context has been successfully cleared');

@@ -267,3 +261,3 @@ }

const exportResult = await fetch(url, {
const exportResult = await doExtrasFetch(url, {
method: 'POST',

@@ -311,3 +305,3 @@ headers: postHeaders,

const importResult = await fetch(url, {
const importResult = await doExtrasFetch(url, {
method: 'POST',

@@ -340,3 +334,3 @@ headers: postHeaders,

const queryMessagesResult = await fetch(url, {
const queryMessagesResult = await doExtrasFetch(url, {
method: 'POST',

@@ -394,3 +388,3 @@ headers: postHeaders,

const addMessagesResult = await fetch(url, {
const addMessagesResult = await doExtrasFetch(url, {
method: 'POST',

@@ -450,3 +444,3 @@ headers: postHeaders,

);
newChat.push(...queriedMessages.map(m => JSON.parse(m.meta)));
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
newChat.push(

@@ -467,3 +461,3 @@ {

//replaces them with chromaDB results (with no separator)
newChat.push(...queriedMessages.map(m => JSON.parse(m.meta)));
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
chat.splice(0, messagesToStore.length, ...newChat);

@@ -492,4 +486,5 @@

<div class="inline-drawer-content">
<p>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</p>
<span>Memory Injection Strategy</span>
<small>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</small>
<span class="wide100p marginTopBot5 displayBlock">Memory Injection Strategy</span>
<hr>
<select id="chromadb_strategy">

@@ -499,9 +494,9 @@ <option value="original">Replace non-kept chat items with memories</option>

</select>
<label for="chromadb_keep_context">How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</label>
<label for="chromadb_keep_context"><small>How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</small></label>
<input id="chromadb_keep_context" type="range" min="${defaultSettings.keep_context_min}" max="${defaultSettings.keep_context_max}" step="${defaultSettings.keep_context_step}" value="${defaultSettings.keep_context}" />
<label for="chromadb_n_results">Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</label>
<label for="chromadb_n_results"><small>Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</small></label>
<input id="chromadb_n_results" type="range" min="${defaultSettings.n_results_min}" max="${defaultSettings.n_results_max}" step="${defaultSettings.n_results_step}" value="${defaultSettings.n_results}" />
<label for="chromadb_split_length">Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</label>
<label for="chromadb_split_length"><small>Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</small></label>
<input id="chromadb_split_length" type="range" min="${defaultSettings.split_length_min}" max="${defaultSettings.split_length_max}" step="${defaultSettings.split_length_step}" value="${defaultSettings.split_length}" />
<label for="chromadb_file_split_length">Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</label>
<label for="chromadb_file_split_length"><small>Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</small></label>
<input id="chromadb_file_split_length" type="range" min="${defaultSettings.file_split_length_min}" max="${defaultSettings.file_split_length_max}" step="${defaultSettings.file_split_length_step}" value="${defaultSettings.file_split_length}" />

@@ -536,3 +531,3 @@ <label class="checkbox_label" for="chromadb_freeze" title="Pauses the automatic synchronization of new messages with ChromaDB. Older messages and injections will still be pulled as usual." >

$('#extensions_settings').append(settingsHtml);
$('#extensions_settings2').append(settingsHtml);
$('#chromadb_strategy').on('change', onStrategyChange);

@@ -539,0 +534,0 @@ $('#chromadb_keep_context').on('input', onKeepContextInput);

@@ -13,3 +13,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern"
}
import { getStringHash, debounce } from "../../utils.js";
import { getContext, getApiUrl, extension_settings } from "../../extensions.js";
import { getContext, getApiUrl, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
import { extension_prompt_types, is_send_press, saveSettingsDebounced } from "../../../script.js";

@@ -47,3 +47,3 @@ export { MODULE_NAME };

}
$('#memory_long_length').val(extension_settings.memory.longMemoryLength).trigger('input');

@@ -133,20 +133,2 @@ $('#memory_short_length').val(extension_settings.memory.shortMemoryLength).trigger('input');

let isWorkerBusy = false;
async function moduleWorkerWrapper() {
// Don't touch me I'm busy...
if (isWorkerBusy) {
return;
}
// I'm free. Let's update!
try {
isWorkerBusy = true;
await moduleWorker();
}
finally {
isWorkerBusy = false;
}
}
async function moduleWorker() {

@@ -156,3 +138,3 @@ const context = getContext();

// no characters or group selected
// no characters or group selected
if (!context.groupId && context.characterId === undefined) {

@@ -256,3 +238,3 @@ return;

const apiResult = await fetch(url, {
const apiResult = await doExtrasFetch(url, {
method: 'POST',

@@ -346,14 +328,14 @@ headers: {

<div class="inline-drawer-toggle inline-drawer-header">
<b>Chat memory</b>
<b>Summarize</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="memory_contents">Memory contents</label>
<label for="memory_contents">Current summary: </label>
<textarea id="memory_contents" class="text_pole" rows="8" placeholder="Context will be generated here..."></textarea>
<div class="memory_contents_controls">
<input id="memory_restore" class="menu_button" type="submit" value="Restore previous state" />
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" /> Freeze context</label>
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Stop summarization updates</label>
</div>
<!--</div>
</div>
</div>
<div class="inline-drawer">

@@ -364,6 +346,6 @@ <div class="inline-drawer-toggle inline-drawer-header">

</div>
<div class="inline-drawer-content">
<label for="memory_short_length">Buffer <small>[short-term]</small> length (<span id="memory_short_length_tokens"></span> tokens)</label>
<div class="inline-drawer-content">-->
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" />
<label for="memory_long_length">Summary <small>[long-term]</small> length (<span id="memory_long_length_tokens"></span> tokens)</label>
<label for="memory_long_length">Summary output length (<span id="memory_long_length_tokens"></span> tokens)</label>
<input id="memory_long_length" type="range" value="${defaultSettings.longMemoryLength}" min="${defaultSettings.minLongMemory}" max="${defaultSettings.maxLongMemory}" step="${defaultSettings.longMemoryStep}" />

@@ -374,3 +356,3 @@ <label for="memory_temperature">Temperature (<span id="memory_temperature_value"></span>)</label>

<input id="memory_repetition_penalty" type="range" value="${defaultSettings.repetitionPenalty}" min="${defaultSettings.minRepetitionPenalty}" max="${defaultSettings.maxRepetitionPenalty}" step="${defaultSettings.repetitionPenaltyStep}" />
<label for="memory_length_penalty">Length penalty <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
<label for="memory_length_penalty">Length preference <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
<input id="memory_length_penalty" type="range" value="${defaultSettings.lengthPenalty}" min="${defaultSettings.minLengthPenalty}" max="${defaultSettings.maxLengthPenalty}" step="${defaultSettings.lengthPenaltyStep}" />

@@ -381,3 +363,3 @@ </div>

`;
$('#extensions_settings').append(settingsHtml);
$('#extensions_settings2').append(settingsHtml);
$('#memory_restore').on('click', onMemoryRestoreClick);

@@ -395,3 +377,4 @@ $('#memory_contents').on('input', onMemoryContentInput);

loadSettings();
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL);
});
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
});

@@ -12,3 +12,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
}
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

@@ -13,3 +13,3 @@ import {

} from "../../../script.js";
import { getApiUrl, getContext, extension_settings, defaultRequestArgs, modules } from "../../extensions.js";
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
import { stringFormat, initScrollHeight, resetScrollHeight } from "../../utils.js";

@@ -238,3 +238,3 @@ export { MODULE_NAME };

url.pathname = '/api/image/model';
const getCurrentModelResult = await fetch(url, {
const getCurrentModelResult = await doExtrasFetch(url, {
method: 'POST',

@@ -290,3 +290,3 @@ headers: postHeaders,

url.pathname = '/api/image/samplers';
const result = await fetch(url, defaultRequestArgs);
const result = await doExtrasFetch(url);

@@ -344,3 +344,3 @@ if (result.ok) {

url.pathname = '/api/image/model';
const getCurrentModelResult = await fetch(url, defaultRequestArgs);
const getCurrentModelResult = await doExtrasFetch(url);

@@ -353,3 +353,3 @@ if (getCurrentModelResult.ok) {

url.pathname = '/api/image/models';
const getModelsResult = await fetch(url, defaultRequestArgs);
const getModelsResult = await doExtrasFetch(url);

@@ -467,3 +467,3 @@ if (getModelsResult.ok) {

case generationMode.FREE:
prompt = processReply(trigger);
prompt = trigger.trim();
break;

@@ -502,3 +502,3 @@ default:

url.pathname = '/api/image';
const result = await fetch(url, {
const result = await doExtrasFetch(url, {
method: 'POST',

@@ -593,4 +593,5 @@ headers: postHeaders,

<div id="sd_dropdown">
<span>Send me a picture of:</span>
<ul class="list-group">
<span>Send me a picture of:</span>
<li class="list-group-item" id="sd_you" data-value="you">Yourself</li>

@@ -619,3 +620,3 @@ <li class="list-group-item" id="sd_face" data-value="face">Your Face</li>

let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
placement: 'bottom',
placement: 'top',
});

@@ -631,6 +632,6 @@

dropdown.show(200);
dropdown.fadeIn(250);
popper.update();
} else {
dropdown.hide(200);
dropdown.fadeOut(250);
}

@@ -646,7 +647,7 @@ });

if (isConnectedToExtras() || extension_settings.sd.horde) {
$('#sd_gen').show(200);
$('#sd_gen').show();
$('.sd_message_gen').show();
}
else {
$('#sd_gen').hide(200);
$('#sd_gen').hide();
$('.sd_message_gen').hide();

@@ -653,0 +654,0 @@ }

@@ -12,3 +12,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
}
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

@@ -10,3 +10,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

@@ -353,3 +353,3 @@ import {

$('#extensionsMenu').append(buttonHtml);
$('#extensions_settings').append(html);
$('#extensions_settings2').append(html);
$('#translate_chat').on('click', onTranslateChatClick);

@@ -356,0 +356,0 @@ $('#translation_clear').on('click', onTranslationsClearClick);

@@ -10,3 +10,3 @@ {

"version": "1.0.0",
"homePage": "https://github.com/Cohee1207/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

@@ -11,3 +11,3 @@ export { ElevenLabsTtsProvider }

separator = ' ... ... ... '
get settings() {

@@ -47,4 +47,4 @@ return this.settings

}
loadSettings(settings) {

@@ -80,4 +80,4 @@ // Pupulate Provider UI given input settings

}
async updateApiKey() {

@@ -114,3 +114,3 @@ // Using this call to validate API key

const historyId = await this.findTtsGenerationInHistory(text, voiceId)
let response

@@ -126,7 +126,7 @@ if (historyId) {

}
//###################//
// Helper Functions //
//###################//
async findTtsGenerationInHistory(message, voiceId) {

@@ -157,3 +157,3 @@ const ttsHistory = await this.fetchTtsHistory()

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}

@@ -175,3 +175,3 @@ const responseJson = await response.json()

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}

@@ -203,3 +203,4 @@ return response.json()

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}

@@ -220,3 +221,3 @@ return response

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}

@@ -234,3 +235,3 @@ return response

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}

@@ -237,0 +238,0 @@ const responseJson = await response.json()

import { callPopup, cancelTtsPlay, eventSource, event_types, isMultigenEnabled, is_send_press, saveSettingsDebounced } from '../../../script.js'
import { extension_settings, getContext } from '../../extensions.js'
import { getStringHash } from '../../utils.js'
import { ModuleWorkerWrapper, extension_settings, getContext } from '../../extensions.js'
import { escapeRegex, getStringHash } from '../../utils.js'
import { EdgeTtsProvider } from './edge.js'
import { ElevenLabsTtsProvider } from './elevenlabs.js'
import { SileroTtsProvider } from './silerotts.js'
import { SystemTtsProvider } from './system.js'
import { NovelTtsProvider } from './novel.js'
import { isMobile } from '../../RossAscends-mods.js'
import { power_user } from '../../power-user.js'

@@ -18,3 +22,43 @@ const UPDATE_INTERVAL = 1000

export function getPreviewString(lang) {
const previewStrings = {
'en-US': 'The quick brown fox jumps over the lazy dog',
'en-GB': 'Sphinx of black quartz, judge my vow',
'fr-FR': 'Portez ce vieux whisky au juge blond qui fume',
'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich',
'it-IT': "Pranzo d'acqua fa volti sghembi",
'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón',
'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky',
'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!',
'pt-BR': 'Vejo xá gritando que fez show sem playback.',
'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.',
'uk-UA': "Фабрикуймо гідність, лящім їжею, ґав хапаймо, з'єднавці чаш!",
'pl-PL': 'Pchnąć w tę łódź jeża lub ośm skrzyń fig',
'cs-CZ': 'Příliš žluťoučký kůň úpěl ďábelské ódy',
'sk-SK': 'Vyhŕňme si rukávy a vyprážajme čínske ryžové cestoviny',
'hu-HU': 'Árvíztűrő tükörfúrógép',
'tr-TR': 'Pijamalı hasta yağız şoföre çabucak güvendi',
'nl-NL': 'De waard heeft een kalfje en een pinkje opgegeten',
'sv-SE': 'Yxskaftbud, ge vårbygd, zinkqvarn',
'da-DK': 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon',
'ja-JP': 'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす',
'ko-KR': '가나다라마바사아자차카타파하',
'zh-CN': '我能吞下玻璃而不伤身体',
'ro-RO': 'Muzicologă în bej vând whisky și tequila, preț fix',
'bg-BG': 'Щъркелите се разпръснаха по цялото небе',
'el-GR': 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός',
'fi-FI': 'Voi veljet, miksi juuri teille myin nämä vehkeet?',
'he-IL': 'הקצינים צעקו: "כל הכבוד לצבא הצבאות!"',
'id-ID': 'Jangkrik itu memang enak, apalagi kalau digoreng',
'ms-MY': 'Muzik penyanyi wanita itu menggambarkan kehidupan yang penuh dengan duka nestapa',
'th-TH': 'เป็นไงบ้างครับ ผมชอบกินข้าวผัดกระเพราหมูกรอบ',
'vi-VN': 'Cô bé quàng khăn đỏ đang ngồi trên bãi cỏ xanh',
'ar-SA': 'أَبْجَدِيَّة عَرَبِيَّة',
'hi-IN': 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा',
}
const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'
return previewStrings[lang] ?? fallbackPreview;
}
let ttsProviders = {

@@ -24,2 +68,4 @@ ElevenLabs: ElevenLabsTtsProvider,

System: SystemTtsProvider,
Edge: EdgeTtsProvider,
Novel: NovelTtsProvider,
}

@@ -30,2 +76,3 @@ let ttsProvider

async function onNarrateOneMessage() {
audioElement.src = '/sounds/silence.mp3';
const context = getContext();

@@ -44,20 +91,2 @@ const id = $(this).closest('.mes').attr('mesid');

let isWorkerBusy = false;
async function moduleWorkerWrapper() {
// Don't touch me I'm busy...
if (isWorkerBusy) {
return;
}
// I'm free. Let's update!
try {
isWorkerBusy = true;
await moduleWorker();
}
finally {
isWorkerBusy = false;
}
}
async function moduleWorker() {

@@ -151,3 +180,3 @@ // Primarily determining when to add new chat to the TTS queue

audioElement.currentTime = 0;
audioElement.src = '';
audioElement.src = '/sounds/silence.mp3';

@@ -199,2 +228,3 @@ // Clear any queue items

let audioElement = new Audio()
audioElement.autoplay = true

@@ -229,3 +259,3 @@ let audioJobQueue = []

if (!$(audio).data('disabled')) {
if (audio && !$(audio).data('disabled')) {
audio.play()

@@ -251,3 +281,5 @@ }

</div>`
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}" data-disabled="${voice.preview_url == false}"></audio>`
if (voice.preview_url) {
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}" data-disabled="${voice.preview_url == false}"></audio>`
}
}

@@ -278,2 +310,3 @@ } catch {

function onAudioControlClicked() {
audioElement.src = '/sounds/silence.mp3';
let context = getContext()

@@ -315,3 +348,3 @@ // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful

const audioData = await response.blob()
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave']) {
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']) {
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`

@@ -390,2 +423,9 @@ }

// Remove character name from start of the line if power user setting is disabled
if (char && !power_user.allow_name2_display) {
debugger;
const escapedChar = escapeRegex(char);
text = text.replace(new RegExp(`^${escapedChar}:`, 'gm'), '');
}
try {

@@ -404,2 +444,3 @@ if (!text) {

if (voiceId == null) {
toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`)
throw `Unable to attain voiceId for ${char}`

@@ -485,3 +526,2 @@ }

let isValidResult = false
const context = getContext()

@@ -677,4 +717,27 @@ const value = $('#tts_voice_map').val()

addAudioControl() // Depends on Extension Controls
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL) // Init depends on all the things
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL) // Init depends on all the things
eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
// Mobiles need to "activate" the Audio element with click before it can be played
if (isMobile()) {
console.debug('Activating mobile audio element on first click');
let audioActivated = false;
// Play silence on first click
$(document).on('click touchend', function () {
// Prevent multiple activations
if (audioActivated) {
return;
}
console.debug('Activating audio element...');
audioActivated = true;
audioElement.src = '/sounds/silence.mp3';
// Reset volume to 1
audioElement.onended = function () {
console.debug('Audio element activated');
};
});
}
})

@@ -6,3 +6,4 @@ {

"optional": [
"tts"
"silero-tts",
"edge-tts"
],

@@ -14,2 +15,2 @@ "js": "index.js",

"homePage": "None"
}
}

@@ -1,2 +0,2 @@

import { getApiUrl, modules } from "../../extensions.js"
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"

@@ -18,3 +18,3 @@ export { SileroTtsProvider }

}
get settingsHtml() {

@@ -25,7 +25,7 @@ let html = `

<span>
<span>Use <a target="_blank" href="https://github.com/Cohee1207/SillyTavern-extras">SillyTavern Extras API</a> or <a target="_blank" href="https://github.com/ouoertheo/silero-api-server">Silero TTS Server</a>.</span>
<span>Use <a target="_blank" href="https://github.com/SillyTavern/SillyTavern-extras">SillyTavern Extras API</a> or <a target="_blank" href="https://github.com/ouoertheo/silero-api-server">Silero TTS Server</a>.</span>
`
return html
}
onSettingsChange() {

@@ -55,3 +55,3 @@ // Used when provider settings are updated from UI

// Use Extras API if TTS support is enabled
if (modules.includes('tts')) {
if (modules.includes('tts') || modules.includes('silero-tts')) {
const baseUrl = new URL(getApiUrl());

@@ -64,3 +64,3 @@ baseUrl.pathname = '/api/tts';

}, 2000);
$('#silero_tts_endpoint').val(this.settings.provider_endpoint)

@@ -70,3 +70,3 @@ console.info("Settings loaded")

async onApplyClick() {

@@ -79,3 +79,3 @@ return

//#################//
async getVoice(voiceName) {

@@ -103,3 +103,3 @@ if (this.voices.length == 0) {

async fetchTtsVoiceIds() {
const response = await fetch(`${this.settings.provider_endpoint}/speakers`)
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`)
if (!response.ok) {

@@ -114,3 +114,3 @@ throw new Error(`HTTP ${response.status}: ${await response.json()}`)

console.info(`Generating new TTS for voice_id ${voiceId}`)
const response = await fetch(
const response = await doExtrasFetch(
`${this.settings.provider_endpoint}/generate`,

@@ -129,3 +129,4 @@ {

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}

@@ -135,18 +136,7 @@ return response

// Interface not used by Silero TTS
async fetchTtsFromHistory(history_item_id) {
console.info(`Fetched existing TTS with history_item_id ${history_item_id}`)
const response = await fetch(
`https://api.elevenlabs.io/v1/history/${history_item_id}/audio`,
{
headers: {
'xi-api-key': this.API_KEY
}
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
return response
return Promise.resolve(history_item_id);
}
}

@@ -0,1 +1,4 @@

import { isMobile } from "../../RossAscends-mods.js";
import { getPreviewString } from "./index.js";
export { SystemTtsProvider }

@@ -77,16 +80,2 @@

previewStrings = {
'en-US': 'The quick brown fox jumps over the lazy dog',
'en-GB': 'Sphinx of black quartz, judge my vow',
'fr-FR': 'Portez ce vieux whisky au juge blond qui fume',
'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich',
'it-IT': "Pranzo d'acqua fa volti sghembi",
'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón',
'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky',
'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!',
'pt-BR': 'Vejo xá gritando que fez show sem playback.',
'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.',
'uk-UA': "Фабрикуймо гідність, лящім їжею, ґав хапаймо, з'єднавці чаш!",
}
fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'
settings

@@ -128,2 +117,17 @@ voices = []

// iOS should only allows speech synthesis trigged by user interaction
if (isMobile()) {
let hasEnabledVoice = false;
document.addEventListener('click', () => {
if (hasEnabledVoice) {
return;
}
const utterance = new SpeechSynthesisUtterance('hi');
utterance.volume = 0;
speechSynthesis.speak(utterance);
hasEnabledVoice = true;
});
}
// Only accept keys defined in defaultSettings

@@ -177,3 +181,3 @@ this.settings = this.defaultSettings;

speechSynthesis.cancel();
const text = this.previewStrings[voice.lang] ?? this.fallbackPreview;
const text = getPreviewString(voice.lang);
const utterance = new SpeechSynthesisUtterance(text);

@@ -180,0 +184,0 @@ utterance.voice = voice;

@@ -5,6 +5,6 @@ ////////////////// LOCAL STORAGE HANDLING /////////////////////

localStorage.setItem(target, val);
console.log('SaveLocal -- ' + target + ' : ' + val);
console.debug('SaveLocal -- ' + target + ' : ' + val);
}
export function LoadLocal(target) {
console.log('LoadLocal -- ' + target);
console.debug('LoadLocal -- ' + target);
return localStorage.getItem(target);

@@ -28,2 +28,2 @@

/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////

@@ -56,4 +56,7 @@ import {

activateSendButtons,
eventSource,
event_types,
getCurrentChatId,
} from "../script.js";
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect } from './tags.js';
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';

@@ -121,3 +124,3 @@ export {

deleteLastMessage();
await deleteLastMessage();
}

@@ -177,5 +180,14 @@

await saveGroupChat(groupId, true);
eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
}
function getFirstCharacterMessage(character) {
let messageText = character.first_mes;
// if there are alternate greetings, pick one at random
if (Array.isArray(character.data?.alternate_greetings)) {
const messageTexts = [character.first_mes, ...character.data.alternate_greetings].filter(x => x);
messageText = messageTexts[Math.floor(Math.random() * messageTexts.length)];
}
const mes = {};

@@ -187,4 +199,6 @@ mes["is_user"] = false;

mes["send_date"] = humanizedDateTime();
mes["mes"] = character.first_mes
? substituteParams(character.first_mes.trim(), name1, character.name)
mes["original_avatar"] = character.avatar;
mes["extra"] = { "gen_id": Date.now() * Math.random() * 1000000 };
mes["mes"] = messageText
? substituteParams(messageText.trim(), name1, character.name)
: default_ch_mes;

@@ -389,2 +403,6 @@ mes["force_avatar"] =

if (is_group_generating) {
return false;
}
// Auto-navigate back to group menu

@@ -396,6 +414,2 @@ if (menu_type !== "group_edit") {

if (is_group_generating) {
return false;
}
const group = groups.find((x) => x.id === selected_group);

@@ -510,3 +524,3 @@ let typingIndicator = $("#chat .typing_indicator");

await saveChatConditional();
$('#send_textarea').val('');
$('#send_textarea').val('').trigger('input');
}

@@ -543,3 +557,3 @@

// if not swipe - check if message generated already
if (type !== "swipe" && !isMultigenEnabled() && chat.length == messagesBefore) {
if (generateType === "group_chat" && !isMultigenEnabled() && chat.length == messagesBefore) {
await delay(100);

@@ -791,2 +805,3 @@ }

selected_group = null;
delete tag_map[id];
resetChatState();

@@ -823,2 +838,4 @@ clearChat();

let groupAutoModeAbortController = null;
async function groupChatAutoModeWorker() {

@@ -839,3 +856,4 @@ if (!is_group_automode_enabled || online_status === "no_connection") {

await generateGroupWrapper(true);
groupAutoModeAbortController = new AbortController();
await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });
}

@@ -961,3 +979,3 @@

template.attr("chid", characters.indexOf(character));
template.addClass(character.fav == 'true' ? 'is_fav' : '');
template.toggleClass('is_fav', character.fav || character.fav == 'true');

@@ -1010,3 +1028,3 @@ if (!group) {

$("#dialogue_popup").data("group_id", groupId);
callPopup("<h3>Delete the group?</h3>", "del_group");
callPopup('<h3>Delete the group?</h3><p>This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.</p>', "del_group");
});

@@ -1093,2 +1111,3 @@

sortGroupMembers("#rm_group_add_members .group_member");
await eventSource.emit(event_types.GROUP_UPDATED);
});

@@ -1416,2 +1435,19 @@ }

function onSendTextareaInput() {
if (is_group_automode_enabled) {
// Wait for current automode generation to finish
is_group_automode_enabled = false;
$("#rm_group_automode").prop("checked", false);
}
}
function stopAutoModeGeneration() {
if (groupAutoModeAbortController) {
groupAutoModeAbortController.abort();
}
is_group_automode_enabled = false;
$("#rm_group_automode").prop("checked", false);
}
jQuery(() => {

@@ -1428,3 +1464,5 @@ $(document).on("click", ".group_select", selectGroup);

is_group_automode_enabled = value;
eventSource.once(event_types.GENERATION_STOPPED, stopAutoModeGeneration);
});
$("#send_textarea").on("keyup", onSendTextareaInput);
});
import {
getRequestHeaders,
saveSettingsDebounced,

@@ -12,2 +13,3 @@ getStoppingStrings,

canUseKoboldStopSequence,
canUseKoboldStreaming,
};

@@ -27,5 +29,7 @@

use_stop_sequence: false,
streaming_kobold: false,
};
const MIN_STOP_SEQUENCE_VERSION = '1.2.2';
const MIN_STREAMING_KCPPVERSION = '1.30';

@@ -63,2 +67,6 @@ function formatKoboldUrl(value) {

}
if (preset.hasOwnProperty('streaming_kobold')) {
kai_settings.streaming_kobold = preset.streaming_kobold;
$('#streaming_kobold').prop('checked', kai_settings.streaming_kobold);
}
}

@@ -92,2 +100,4 @@

stop_sequence: kai_settings.use_stop_sequence ? getStoppingStrings(isImpersonate, false) : undefined,
streaming: kai_settings.streaming_kobold && kai_settings.can_use_streaming,
can_abort: kai_settings.can_use_streaming,
};

@@ -97,2 +107,44 @@ return generate_data;

export async function generateKoboldWithStreaming(generate_data, signal) {
const response = await fetch('/generate', {
headers: getRequestHeaders(),
body: JSON.stringify(generate_data),
method: 'POST',
signal: signal,
});
return async function* streamData() {
const decoder = new TextDecoder();
const reader = response.body.getReader();
let getMessage = '';
let messageBuffer = "";
while (true) {
const { done, value } = await reader.read();
let response = decoder.decode(value);
let eventList = [];
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
messageBuffer += response;
eventList = messageBuffer.split("\n\n");
// Last element will be an empty string or a leftover partial message
messageBuffer = eventList.pop();
for (let event of eventList) {
for (let subEvent of event.split('\n')) {
if (subEvent.startsWith("data")) {
let data = JSON.parse(subEvent.substring(5));
getMessage += (data?.token || '');
yield getMessage;
}
}
}
if (done) {
return;
}
}
}
}
const sliders = [

@@ -168,2 +220,8 @@ {

function canUseKoboldStreaming(koboldVersion) {
if (koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_STREAMING_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false;
}
$(document).ready(function () {

@@ -185,2 +243,8 @@ sliders.forEach(slider => {

});
});
$('#streaming_kobold').on("input", function () {
const value = $(this).prop('checked');
kai_settings.streaming_kobold = value;
saveSettingsDebounced();
});
});
import {
getRequestHeaders,
saveSettingsDebounced,

@@ -22,2 +23,3 @@ } from "../script.js";

preset_settings_novel: "Classic-Euterpe",
streaming_novel: false,
};

@@ -69,2 +71,3 @@

nai_settings.tail_free_sampling_novel = settings.tail_free_sampling_novel;
nai_settings.streaming_novel = !!settings.streaming_novel;
loadNovelSettingsUi(nai_settings);

@@ -88,2 +91,3 @@ }

$("#tail_free_sampling_counter_novel").text(Number(ui_settings.tail_free_sampling_novel).toFixed(3));
$("#streaming_novel").prop('checked', ui_settings.streaming_novel);
}

@@ -156,11 +160,54 @@

//bad_words_ids = {{50256}, {0}, {1}};
//generate_until_sentence = true;
"generate_until_sentence": true,
"use_cache": false,
//use_string = true;
"use_string": true,
"return_full_text": false,
"prefix": "vanilla",
"order": this_settings.order
"order": this_settings.order,
"streaming": nai_settings.streaming_novel,
};
}
export async function generateNovelWithStreaming(generate_data, signal) {
const response = await fetch('/generate_novelai', {
headers: getRequestHeaders(),
body: JSON.stringify(generate_data),
method: 'POST',
signal: signal,
});
return async function* streamData() {
const decoder = new TextDecoder();
const reader = response.body.getReader();
let getMessage = '';
let messageBuffer = "";
while (true) {
const { done, value } = await reader.read();
let response = decoder.decode(value);
let eventList = [];
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
messageBuffer += response;
eventList = messageBuffer.split("\n\n");
// Last element will be an empty string or a leftover partial message
messageBuffer = eventList.pop();
for (let event of eventList) {
for (let subEvent of event.split('\n')) {
if (subEvent.startsWith("data")) {
let data = JSON.parse(subEvent.substring(5));
getMessage += (data?.token || '');
yield getMessage;
}
}
}
if (done) {
return;
}
}
}
}
$(document).ready(function () {

@@ -178,2 +225,8 @@ sliders.forEach(slider => {

$('#streaming_novel').on('input', function () {
const value = !!$(this).prop('checked');
nai_settings.streaming_novel = value;
saveSettingsDebounced();
});
$("#model_novel_select").change(function () {

@@ -180,0 +233,0 @@ nai_settings.model_novel = $("#model_novel_select").find(":selected").val();

@@ -6,5 +6,7 @@ import {

substituteParams,
getRequestHeaders,
max_context,
getTokenCount,
getRequestHeaders,
eventSource,
event_types,
scrollChatToBottom,
} from "../script.js";

@@ -16,2 +18,3 @@ import {

} from "./secrets.js";
import { delay, splitRecursive } from "./utils.js";

@@ -26,2 +29,4 @@ export {

const POE_TOKEN_LENGTH = 2048;
const CHUNKED_PROMPT_LENGTH = POE_TOKEN_LENGTH * 3.35;
const MAX_RETRIES_FOR_ACTIVATION = 5;

@@ -58,8 +63,10 @@ const DEFAULT_JAILBREAK_RESPONSE = 'Understood.';

streaming: false,
suggest: false,
};
let auto_jailbroken = false;
let got_reply = false;
let messages_to_purge = 0;
let is_get_status_poe = false;
let is_poe_button_press = false;
let abortControllerSuggest = null;

@@ -79,5 +86,11 @@ function loadPoeSettings(settings) {

$('#poe_impersonation_prompt').val(poe_settings.impersonation_prompt);
$('#poe_suggest').prop('checked', poe_settings.suggest);
selectBot();
}
function abortSuggestedReplies() {
abortControllerSuggest && abortControllerSuggest.abort();
$('.last_mes .suggested_replies').remove();
}
function selectBot() {

@@ -89,2 +102,87 @@ if (poe_settings.bot) {

function onSuggestedReplyClick() {
const reply = $(this).find('.suggested_reply_text').text();
$("#send_textarea").val(reply);
$("#send_but").trigger('click');
}
function appendSuggestedReply(reply) {
if ($('.last_mes .suggested_replies').length === 0) {
$('.last_mes .mes_block').append(`
<div class="suggested_replies">
</div>
`);
}
const newElement = $(`<div class="suggested_reply"><div class="suggested_reply_text">${reply}</div></div>`);
newElement.hide();
$('.last_mes .suggested_replies').append(newElement);
newElement.fadeIn(500, async () => {
await delay(1);
scrollChatToBottom();
});
}
async function suggestReplies(messageId) {
// If the feature is disabled
if (!poe_settings.suggest) {
return;
}
// Cancel previous request
if (abortControllerSuggest) {
abortControllerSuggest.abort();
}
abortControllerSuggest = new AbortController();
abortControllerSuggest.signal.addEventListener('abort', () => {
// Hide suggestion UI
});
console.log('Querying suggestions for message', messageId);
const response = await fetch(`/poe_suggest`, {
method: 'POST',
signal: abortControllerSuggest.signal,
headers: getRequestHeaders(),
body: JSON.stringify({
messageId: messageId,
bot: poe_settings.bot,
}),
});
const decodeSuggestions = async function* () {
const decoder = new TextDecoder();
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
let response = decoder.decode(value);
const replies = response.split('\n\n');
for (let i = 0; i < replies.length - 1; i++) {
if (replies[i]) {
yield replies[i];
}
}
if (done) {
return;
}
}
}
const suggestions = [];
for await (const suggestion of decodeSuggestions()) {
suggestions.push(suggestion);
console.log('Got suggestion:', [suggestion]);
appendSuggestedReply(suggestion);
}
return suggestions;
}
function onBotChange() {

@@ -112,9 +210,46 @@ poe_settings.bot = $('#poe_bots').find(":selected").val();

async function onPurgeChatClick() {
toastr.info('Purging the conversation. Please wait...');
await purgeConversation();
toastr.success('Conversation purged! Jailbreak the bot to continue.');
auto_jailbroken = false;
messages_to_purge = 0;
}
async function onSendJailbreakClick() {
auto_jailbroken = false;
toastr.info('Sending jailbreak message. Please wait...');
await autoJailbreak();
if (auto_jailbroken) {
toastr.success('Jailbreak successful!');
} else {
toastr.error('Jailbreak unsuccessful!');
}
}
async function autoJailbreak() {
for (let retryNumber = 0; retryNumber < MAX_RETRIES_FOR_ACTIVATION; retryNumber++) {
const reply = await sendMessage(substituteParams(poe_settings.jailbreak_message), false, false);
if (reply.toLowerCase().includes(poe_settings.jailbreak_response.toLowerCase())) {
auto_jailbroken = true;
break;
}
}
}
async function generatePoe(type, finalPrompt, signal) {
if (poe_settings.auto_purge) {
let count_to_delete = -1;
console.debug('Auto purge is enabled');
let count_to_delete = 0;
if (auto_jailbroken && got_reply) {
count_to_delete = 2;
if (auto_jailbroken) {
console.debug(`Purging ${messages_to_purge} messages`);
count_to_delete = messages_to_purge;
}
else {
console.debug('Purging all messages');
count_to_delete = -1;
}

@@ -124,15 +259,10 @@ await purgeConversation(count_to_delete);

if (poe_settings.auto_jailbreak && !auto_jailbroken) {
for (let retryNumber = 0; retryNumber < MAX_RETRIES_FOR_ACTIVATION; retryNumber++) {
const reply = await sendMessage(substituteParams(poe_settings.jailbreak_message), false);
if (reply.toLowerCase().includes(poe_settings.jailbreak_response.toLowerCase())) {
auto_jailbroken = true;
break;
}
if (!auto_jailbroken) {
if (poe_settings.auto_jailbreak) {
console.debug('Attempting auto-jailbreak');
await autoJailbreak();
} else {
console.debug('Auto jailbreak is disabled');
}
}
else {
auto_jailbroken = false;
}

@@ -144,8 +274,53 @@ if (poe_settings.auto_jailbreak && !auto_jailbroken) {

const isQuiet = type === 'quiet';
const reply = await sendMessage(finalPrompt, !isQuiet, signal);
got_reply = true;
let reply = '';
if (max_context > POE_TOKEN_LENGTH && poe_settings.bot !== 'a2_100k') {
console.debug('Prompt is too long, sending in chunks');
const result = await sendChunkedMessage(finalPrompt, !isQuiet, signal)
reply = result.reply;
messages_to_purge = result.chunks + 1; // +1 for the reply
}
else {
console.debug('Sending prompt in one message');
reply = await sendMessage(finalPrompt, !isQuiet, !isQuiet, signal);
messages_to_purge = 2; // prompt and the reply
}
return reply;
}
async function sendChunkedMessage(finalPrompt, withStreaming, signal) {
const fastReplyPrompt = '\n[Reply to this message with a full stop only]';
const promptChunks = splitRecursive(finalPrompt, CHUNKED_PROMPT_LENGTH - fastReplyPrompt.length);
console.debug(`Splitting prompt into ${promptChunks.length} chunks`, promptChunks);
let reply = '';
for (let i = 0; i < promptChunks.length; i++) {
let promptChunk = promptChunks[i];
console.debug(`Sending chunk ${i + 1}/${promptChunks.length}: ${promptChunk}`);
if (i == promptChunks.length - 1) {
// Extract reply of the last chunk
reply = await sendMessage(promptChunk, withStreaming, true, signal);
} else {
// Add fast reply prompt to the chunk
promptChunk += fastReplyPrompt;
// Send chunk without streaming
const chunkReply = await sendMessage(promptChunk, false, false, signal);
console.debug('Got chunk reply: ' + chunkReply);
// Delete the reply for the chunk
await purgeConversation(1);
}
}
return { reply: reply, chunks: promptChunks.length };
}
// If count is -1, purge all messages
// If count is 0, do nothing
// If count is > 0, purge that many messages
async function purgeConversation(count = -1) {
if (count == 0) {
return true;
}
const body = JSON.stringify({

@@ -165,3 +340,3 @@ bot: poe_settings.bot,

async function sendMessage(prompt, withStreaming, signal) {
async function sendMessage(prompt, withStreaming, withSuggestions, signal) {
if (!signal) {

@@ -184,2 +359,5 @@ signal = new AbortController().signal;

const messageId = response.headers.get('X-Message-Id');
if (withStreaming && poe_settings.streaming) {

@@ -196,2 +374,7 @@ return async function* streamData() {

if (done) {
// Start suggesting only once the message is fully received
if (messageId && withSuggestions && poe_settings.suggest) {
suggestReplies(messageId);
}
return;

@@ -207,2 +390,6 @@ }

if (response.ok) {
if (messageId && withSuggestions && poe_settings.suggest) {
suggestReplies(messageId);
}
const data = await response.json();

@@ -233,3 +420,3 @@ return data.reply;

if (is_poe_button_press) {
console.log('Poe API button is pressed');
console.debug('Poe API button is pressed');
return;

@@ -277,2 +464,4 @@ }

setOnlineStatus('Connected!');
eventSource.on(event_types.CHAT_CHANGED, abortSuggestedReplies);
eventSource.on(event_types.MESSAGE_SWIPED, abortSuggestedReplies);
}

@@ -290,3 +479,3 @@ else {

auto_jailbroken = false;
got_reply = false;
messages_to_purge = 0;
}

@@ -329,2 +518,11 @@

function onSuggestInput() {
poe_settings.suggest = !!$(this).prop('checked');
saveSettingsDebounced();
if (!poe_settings.suggest) {
abortSuggestedReplies();
}
}
function onImpersonationPromptInput() {

@@ -374,2 +572,6 @@ poe_settings.impersonation_prompt = $(this).val();

$('#poe_activation_message_restore').on('click', onMessageRestoreClick);
$('#poe_send_jailbreak').on('click', onSendJailbreakClick);
$('#poe_purge_chat').on('click', onPurgeChatClick);
$('#poe_suggest').on('input', onSuggestInput);
$(document).on('click', '.suggested_reply', onSuggestedReplyClick);
});

@@ -12,2 +12,6 @@ import {

updateVisibleDivs,
eventSource,
event_types,
getCurrentChatId,
is_send_press,
} from "../script.js";

@@ -20,2 +24,4 @@ import { favsToHotswap } from "./RossAscends-mods.js";

import { registerSlashCommand } from "./slash-commands.js";
export {

@@ -63,2 +69,4 @@ loadPowerUserSettings,

LLAMA: 3,
NERD: 4,
NERD2: 5,
}

@@ -130,2 +138,4 @@

max_context_unlocked: false,
prefer_character_prompt: true,
prefer_character_jailbreak: true,

@@ -143,3 +153,6 @@ instruct: {

separator_sequence: '',
}
},
personas: {},
default_persona: null,
};

@@ -249,5 +262,8 @@

function toggleWaifu() {
$("#waifuMode").trigger("click");
}
function switchWaifuMode() {
const waifuMode = localStorage.getItem(storage_keys.waifuMode);
power_user.waifuMode = waifuMode === null ? false : waifuMode == "true";
console.log(`switching waifu to ${power_user.waifuMode}`);
$("body").toggleClass("waifuMode", power_user.waifuMode);

@@ -456,3 +472,2 @@ $("#waifuMode").prop("checked", power_user.waifuMode);

applyChatDisplay();
switchWaifuMode()
switchMovingUI();

@@ -479,3 +494,2 @@ noShadows();

const fastUi = localStorage.getItem(storage_keys.fast_ui_mode);
const waifuMode = localStorage.getItem(storage_keys.waifuMode);
const movingUI = localStorage.getItem(storage_keys.movingUI);

@@ -486,3 +500,2 @@ const noShadows = localStorage.getItem(storage_keys.noShadows);

power_user.fast_ui_mode = fastUi === null ? true : fastUi == "true";
power_user.waifuMode = waifuMode === null ? false : waifuMode == "true";
power_user.movingUI = movingUI === null ? false : movingUI == "true";

@@ -535,2 +548,4 @@ power_user.noShadows = noShadows === null ? false : noShadows == "true";

$("#messageTimerEnabled").prop("checked", power_user.timer_enabled);
$("#prefer_character_prompt").prop("checked", power_user.prefer_character_prompt);
$("#prefer_character_jailbreak").prop("checked", power_user.prefer_character_jailbreak);
$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop("checked", true);

@@ -570,2 +585,3 @@ $(`input[name="chat_display"][value="${power_user.chat_display}"]`).prop("checked", true);

loadMaxContextUnlocked();
switchWaifuMode();
}

@@ -672,7 +688,9 @@

export function formatInstructStoryString(story) {
export function formatInstructStoryString(story, systemPrompt) {
// If the character has a custom system prompt AND user has it preferred, use that instead of the default
systemPrompt = power_user.prefer_character_prompt && systemPrompt ? systemPrompt : power_user.instruct.system_prompt;
const sequence = power_user.instruct.system_sequence || '';
const prompt = substituteParams(power_user.instruct.system_prompt) || '';
const prompt = substituteParams(systemPrompt) || '';
const separator = power_user.instruct.wrap ? '\n' : '';
const textArray = [sequence, prompt, story, separator];
const textArray = [sequence, prompt + '\n' + story, separator];
const text = textArray.filter(x => x).join(separator);

@@ -744,3 +762,3 @@ return text;

}
updateVisibleDivs();
updateVisibleDivs('#rm_print_characters_block', true);
}

@@ -862,4 +880,23 @@

document.getElementById("WorldInfo").style.margin = '';
$('*[data-dragged="true"]').removeAttr('data-dragged');
eventSource.emit(event_types.MOVABLE_PANELS_RESET);
}
function doNewChat() {
setTimeout(() => {
$("#option_start_new_chat").trigger('click');
}, 1);
//$("#dialogue_popup").hide();
setTimeout(() => {
$("#dialogue_popup_ok").trigger('click');
}, 1);
}
function doDelMode() {
setTimeout(() => {
$("#option_delete_mes").trigger('click')
}, 1);
}
$(document).ready(() => {

@@ -937,2 +974,3 @@ // Settings that go to settings.json

saveSettingsDebounced();
reloadMarkdownProcessor(power_user.render_formulas);
});

@@ -952,5 +990,5 @@

$("#waifuMode").change(function () {
power_user.waifuMode = $(this).prop("checked");
localStorage.setItem(storage_keys.waifuMode, power_user.waifuMode);
$("#waifuMode").on('change', () => {
power_user.waifuMode = $('#waifuMode').prop("checked");
saveSettingsDebounced();
switchWaifuMode();

@@ -1161,2 +1199,9 @@ });

$("#reload_chat").on('click', function () {
const currentChatId = getCurrentChatId();
if (currentChatId !== undefined && currentChatId !== null) {
reloadCurrentChat();
}
});
$("#allow_name1_display").on("input", function () {

@@ -1193,2 +1238,14 @@ power_user.allow_name1_display = !!$(this).prop('checked');

$("#prefer_character_prompt").on("input", function () {
const value = !!$(this).prop('checked');
power_user.prefer_character_prompt = value;
saveSettingsDebounced();
});
$("#prefer_character_jailbreak").on("input", function () {
const value = !!$(this).prop('checked');
power_user.prefer_character_jailbreak = value;
saveSettingsDebounced();
});
$(window).on('focus', function () {

@@ -1201,2 +1258,6 @@ browser_has_focus = true;

});
registerSlashCommand('vn', toggleWaifu, ['vn'], ' – swaps Visual Novel Mode On/Off', false, true);
registerSlashCommand('newchat', doNewChat, ['newchat'], ' – start a new chat with current character', true, true);
registerSlashCommand('delmode', doDelMode, ['delmode'], ' – enter message deletion mode', true, true);
});

@@ -24,3 +24,3 @@ esversion: 6

import { LoadLocal, SaveLocal, ClearLocal, CheckLocal, LoadLocalBool } from "./f-localStorage.js";
import { LoadLocal, SaveLocal, CheckLocal, LoadLocalBool } from "./f-localStorage.js";
import { selected_group, is_group_generating, getGroupAvatar, groups } from "./group-chats.js";

@@ -31,3 +31,4 @@ import {

} from "./secrets.js";
import { sortByCssOrder } from "./utils.js";
import { sortByCssOrder, debounce } from "./utils.js";
import { chat_completion_sources, oai_settings } from "./openai.js";

@@ -65,2 +66,3 @@ var NavToggle = document.getElementById("nav-toggle");

const observerConfig = { childList: true, subtree: true };
const countTokensDebounced = debounce(RA_CountCharTokens, 1000);

@@ -76,3 +78,2 @@ const observer = new MutationObserver(function (mutations) {

}
});

@@ -86,3 +87,3 @@ });

* @param {String} querySelector - Selector of element to wait for
* @param {Integer} timeout - Milliseconds to wait before timing out, or 0 for no timeout
* @param {Integer} timeout - Milliseconds to wait before timing out, or 0 for no timeout
*/

@@ -199,5 +200,5 @@ function waitForElement(querySelector, timeout) {

//when any input is made to the create/edit character form textareas
$("#rm_ch_create_block").on("input", function () { RA_CountCharTokens(); });
$("#rm_ch_create_block").on("input", function () { countTokensDebounced(); });
//when any input is made to the advanced editing popup textareas
$("#character_popup").on("input", function () { RA_CountCharTokens(); });
$("#character_popup").on("input", function () { countTokensDebounced(); });
//function:

@@ -268,12 +269,19 @@ export function RA_CountCharTokens() {

perm_tokens = getTokenCount(perm_string);
} else { console.log("RA_TC -- no valid char found, closing."); } // if neither, probably safety char or some error in loading
// if neither, probably safety char or some error in loading
} else { console.debug("RA_TC -- no valid char found, closing."); }
}
// display the counted tokens
if (count_tokens < 1024 && perm_tokens < 1024) {
$("#result_info").html(count_tokens + " Tokens (" + perm_tokens + " Permanent Tokens)"); //display normal if both counts are under 1024
//display normal if both counts are under 1024
$("#result_info").html(`<small>${count_tokens} Tokens (${perm_tokens} Permanent)</small>`);
} else {
$("#result_info").html(`
<span class="neutral_warning">${count_tokens}</span>&nbsp;Tokens (<span class="neutral_warning">${perm_tokens}</span><span>&nbsp;Permanent Tokens)
<br>
<div id="chartokenwarning" class="menu_button whitespacenowrap"><a href="/notes#charactertokens" target="_blank">Learn More About Token 'Limits'</a></div>`);
<div class="flex-container flexFlowColumn alignitemscenter">
<div class="flex-container flexnowrap flexNoGap">
<small class="flex-container flexnowrap flexNoGap">
<div class="neutral_warning">${count_tokens}</div>&nbsp;Tokens (<div class="neutral_warning">${perm_tokens}</div><div>&nbsp;Permanent)</div>
</small>
</div>
<div id="chartokenwarning" class="menu_button whitespacenowrap"><a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-tokens" target="_blank">About Token 'Limits'</a></div>
</div>`);
} //warn if either are over 1024

@@ -293,3 +301,3 @@ }

// if the charcter list hadn't been loaded yet, try again.
// if the charcter list hadn't been loaded yet, try again.
} else { setTimeout(RA_autoloadchat, 100); }

@@ -359,3 +367,3 @@ }

if (online_status !== undefined && online_status !== "no_connection") {
$("#send_textarea").attr("placeholder", "Type a message..."); //on connect, placeholder tells user to type message
$("#send_textarea").attr("placeholder", `Type a message, or /? for command list`); //on connect, placeholder tells user to type message
$('#send_form').removeClass("no-connection");

@@ -400,3 +408,3 @@ $("#API-status-top").removeClass("fa-plug-circle-exclamation redOverlayGlow");

case 'openai':
if (secret_state[SECRET_KEYS.OPENAI]) {
if (secret_state[SECRET_KEYS.OPENAI] || secret_state[SECRET_KEYS.CLAUDE] || oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
$("#api_button_openai").click();

@@ -441,3 +449,3 @@ }

if (LoadLocalBool("LNavLockOn") == true && LoadLocalBool("LNavOpened") == true) {
console.log("RA -- clicking left nav to open");
console.debug("RA -- clicking left nav to open");
$("#leftNavDrawerIcon").click();

@@ -448,3 +456,3 @@ }

if (LoadLocalBool("WINavLockOn") == true && LoadLocalBool("WINavOpened") == true) {
console.log("RA -- clicking WI to open");
console.debug("RA -- clicking WI to open");
$("#WIDrawerIcon").click();

@@ -465,3 +473,3 @@ }

function dragElement(elmnt) {
export function dragElement(elmnt) {

@@ -520,4 +528,4 @@ var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

elmnt.setAttribute('data-dragged', 'true');
//fix over/underflows:

@@ -580,6 +588,6 @@

sheldWidth: ${sheldWidth}
X: ${elmnt.style.left}
Y: ${elmnt.style.top}
MaxX: ${maxX}, MaxY: ${maxY}
Topbar 1st X: ${((winWidth - sheldWidth) / 2)}
X: ${elmnt.style.left}
Y: ${elmnt.style.top}
MaxX: ${maxX}, MaxY: ${maxY}
Topbar 1st X: ${((winWidth - sheldWidth) / 2)}
TopBar lastX: ${((winWidth - sheldWidth) / 2) + sheldWidth}

@@ -669,10 +677,10 @@ `); */

if ($(WIPanelPin).prop("checked") == true) {
console.log('adding pin class to WI');
console.debug('adding pin class to WI');
$(WorldInfo).addClass('pinnedOpen');
} else {
console.log('removing pin class from WI');
console.debug('removing pin class from WI');
$(WorldInfo).removeClass('pinnedOpen');
if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {
console.log('closing WI after lock removal');
console.debug('closing WI after lock removal');
$(WorldInfo).slideToggle(200, "swing");

@@ -692,3 +700,3 @@ //$(WorldInfoDrawerIcon).toggleClass('openIcon closedIcon');

if ($(RPanelPin).prop('checked' == true)) {
console.log('setting pin class via checkbox state');
console.debug('setting pin class via checkbox state');
$(RightNavPanel).addClass('pinnedOpen');

@@ -703,3 +711,3 @@ }

if ($(LPanelPin).prop('checked' == true)) {
console.log('setting pin class via checkbox state');
console.debug('setting pin class via checkbox state');
$(LeftNavPanel).addClass('pinnedOpen');

@@ -716,3 +724,3 @@ }

if ($(WIPanelPin).prop('checked' == true)) {
console.log('setting pin class via checkbox state');
console.debug('setting pin class via checkbox state');
$(WorldInfo).addClass('pinnedOpen');

@@ -773,3 +781,3 @@ }

//this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
//this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
$('#send_textarea').on('input', function () {

@@ -828,12 +836,36 @@ this.style.height = '40px';

}
//ctrl+shift+up to scroll to context line
if (event.shiftKey && event.ctrlKey && event.key == "ArrowUp") {
event.preventDefault();
let contextLine = $('.lastInContext');
if (contextLine.length !== 0) {
$('#chat').animate({
scrollTop: contextLine.offset().top - $('#chat').offset().top + $('#chat').scrollTop()
}, 300);
} else { toastr.warning('Context line not found, send a message first!'); }
}
//ctrl+shift+down to scroll to bottom of chat
if (event.shiftKey && event.ctrlKey && event.key == "ArrowDown") {
event.preventDefault();
$('#chat').animate({
scrollTop: $('#chat').prop('scrollHeight')
}, 300);
}
// Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
if (event.ctrlKey && event.key == "Enter") {
// Ctrl+Enter for Regeneration Last Response
if (is_send_press == false) {
const editMesDone = $(".mes_edit_done:visible");
if (editMesDone.length > 0) {
console.debug("Accepting edits with Ctrl+Enter");
editMesDone.trigger('click');
} else if (is_send_press == false) {
console.debug("Regenerating with Ctrl+Enter");
$('#option_regenerate').click();
$('#options').hide();
} else {
console.debug("Ctrl+Enter ignored");
}
}
if (event.ctrlKey && event.key == "ArrowLeft") { //for debug, show all local stored vars
//ctrl+left to show all local stored vars (debug)
if (event.ctrlKey && event.key == "ArrowLeft") {
CheckLocal();

@@ -866,3 +898,3 @@ }

if (event.ctrlKey && event.key == "ArrowUp") { //edits last USER message if chatbar is empty and focused
console.log('got ctrl+uparrow input');
console.debug('got ctrl+uparrow input');
if (

@@ -869,0 +901,0 @@ $("#send_textarea").val() === '' &&

@@ -8,2 +8,3 @@ import { callPopup, getRequestHeaders } from "../script.js";

NOVEL: 'api_key_novel',
CLAUDE: 'api_key_claude',
}

@@ -16,2 +17,3 @@

[SECRET_KEYS.NOVEL]: '#api_key_novel',
[SECRET_KEYS.CLAUDE]: '#api_key_claude',
}

@@ -25,2 +27,3 @@

$(INPUT_MAP[key]).val('');
$('#main_api').trigger('change');
}

@@ -31,3 +34,3 @@

const validSecret = !!secret_state[secret_key];
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
$(input_selector).attr('placeholder', placeholder);

@@ -57,3 +60,3 @@ }

$(table).append('<thead><th>Key</th><th>Value</th></thead>');
for (const [key,value] of Object.entries(data)) {

@@ -108,2 +111,2 @@ $(table).append(`<tr><td>${DOMPurify.sanitize(key)}</td><td>${DOMPurify.sanitize(value)}</td></tr>`);

$(document).on('click', '.clear-api-key', clearSecret);
});
});
import {
addOneMessage,
autoSelectPersona,
characters,

@@ -14,2 +15,4 @@ chat,

sendSystemMessage,
setUserName,
substituteParams,
system_avatar,

@@ -19,2 +22,3 @@ system_message_types

import { humanizedDateTime } from "./RossAscends-mods.js";
import { power_user } from "./power-user.js";
export {

@@ -97,2 +101,5 @@ executeSlashCommands,

parser.addCommand('help', helpCommandCallback, ['?'], ' – displays this help message', true, true);
parser.addCommand('name', setNameCallback, ['persona'], '<span class="monospace">(name)</span> – sets user name and persona avatar (if set)', true, true);
parser.addCommand('sync', syncCallback, [], ' – syncs user name in user-attributed messages in the current chat', true, true);
parser.addCommand('lock', bindCallback, ['bind'], ' – locks/unlocks a persona (name and avatar) to the current chat', true, true);
parser.addCommand('bg', setBackgroundCallback, ['background'], '<span class="monospace">(filename)</span> – sets a background according to filename, partial names allowed, will set the first one alphabetically if multiple files begin with the provided argument string', false, true);

@@ -106,2 +113,27 @@ parser.addCommand('sendas', sendMessageAs, [], ` – sends message as a specific character.<br>Example:<br><pre><code>/sendas Chloe\nHello, guys!</code></pre>will send "Hello, guys!" from "Chloe".<br>Uses character avatar if it exists in the characters list.`, true, true);

function syncCallback() {
$('#sync_name_button').trigger('click');
}
function bindCallback() {
$('#lock_user_name').trigger('click');
}
function setNameCallback(_, name) {
if (!name) {
return;
}
name = name.trim();
// If the name is a persona, auto-select it
if (Object.values(power_user.personas).map(x => x.toLowerCase()).includes(name.toLowerCase())) {
autoSelectPersona(name);
}
// Otherwise, set just the name
else {
setUserName(name);
}
}
function setNarratorName(_, text) {

@@ -150,3 +182,3 @@ const name = text || NARRATOR_NAME_DEFAULT;

send_date: humanizedDateTime(),
mes: mesText,
mes: substituteParams(mesText),
force_avatar: force_avatar,

@@ -182,3 +214,3 @@ original_avatar: original_avatar,

send_date: humanizedDateTime(),
mes: text.trim(),
mes: substituteParams(text.trim()),
force_avatar: system_avatar,

@@ -239,2 +271,3 @@ extra: {

console.debug('Slash command executing:', result);
result.command.callback(result.args, result.value);

@@ -241,0 +274,0 @@

@@ -29,7 +29,12 @@ import {

const ACTIONABLE_TAGS = {
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-tags' },
FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: applyFavFilter, icon: 'fa-solid fa-star' },
GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users' },
HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags' },
}
const InListActionable = {
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear' },
}
const DEFAULT_TAGS = [

@@ -63,3 +68,3 @@ { id: random_id(), name: "Plain Text" },

});
updateVisibleDivs();
updateVisibleDivs('#rm_print_characters_block', true);
}

@@ -76,3 +81,3 @@

});
updateVisibleDivs();
updateVisibleDivs('#rm_print_characters_block', true);
}

@@ -239,2 +244,5 @@

}
if (action && tag.id === 2) {
tagElement.addClass('innerActionable hidden');
}

@@ -252,3 +260,3 @@ $(listElement).append(tagElement);

$(CHARACTER_SELECTOR).each((_, element) => applyFilterToElement(tagIds, element));
updateVisibleDivs();
updateVisibleDivs('#rm_print_characters_block', true);
}

@@ -299,2 +307,5 @@

for (const tag of Object.values(InListActionable)) {
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action });
}
for (const tag of tagsToDisplay) {

@@ -435,3 +446,3 @@ appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: true, });

function onTagColorize(evt) {
console.log(evt);
console.debug(evt);
const id = $(evt.target).closest('.tag_view_item').attr('id');

@@ -443,6 +454,13 @@ const newColor = evt.detail.rgba;

tag.color = newColor;
console.log(tag);
console.debug(tag);
saveSettingsDebounced();
}
function onTagListHintClick() {
console.log($(this));
$(this).toggleClass('selected');
$(this).siblings(".tag:not(.actionable)").toggle(100);
$(this).siblings(".innerActionable").toggleClass('hidden');
}
$(document).ready(() => {

@@ -449,0 +467,0 @@ createTagInput('#tagInput', '#tagList');

@@ -0,1 +1,3 @@

import { getContext } from "./extensions.js";
export function onlyUnique(value, index, array) {

@@ -96,2 +98,13 @@ return array.indexOf(value) === index;

export function throttle(func, limit = 300) {
let lastCall;
return (...args) => {
const now = Date.now();
if (!lastCall || (now - lastCall) >= limit) {
lastCall = now;
func.apply(this, args);
}
};
}
export function isElementInViewport(el) {

@@ -223,3 +236,3 @@ if (typeof jQuery === "function" && el instanceof jQuery) {

const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$']); // extend this as you see fit
const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '!', '?', '”', ')', '】', '】', '’', '」', '】']); // extend this as you see fit
let last = -1;

@@ -261,3 +274,3 @@

export function isOdd(number) {
return number % 2 !== 0;
return number % 2 !== 0;
}

@@ -320,1 +333,109 @@

}
export class IndexedDBStore {
constructor(dbName, storeName) {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore(this.storeName, { keyPath: null, autoIncrement: false });
};
request.onsuccess = (event) => {
console.debug(`IndexedDBStore.open(${this.dbName})`);
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => {
console.error(`IndexedDBStore.open(${this.dbName})`);
reject(event.target.error);
};
});
}
async get(key) {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readonly");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.get(key);
request.onsuccess = (event) => {
console.debug(`IndexedDBStore.get(${key})`);
resolve(event.target.result);
};
request.onerror = (event) => {
console.error(`IndexedDBStore.get(${key})`);
reject(event.target.error);
};
});
}
async put(key, object) {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.put(object, key);
request.onsuccess = (event) => {
console.debug(`IndexedDBStore.put(${key})`);
resolve(event.target.result);
};
request.onerror = (event) => {
console.error(`IndexedDBStore.put(${key})`);
reject(event.target.error);
};
});
}
async delete(key) {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.delete(key);
request.onsuccess = (event) => {
console.debug(`IndexedDBStore.delete(${key})`);
resolve(event.target.result);
};
request.onerror = (event) => {
console.error(`IndexedDBStore.delete(${key})`);
reject(event.target.error);
};
});
}
}
export function isDataURL(str) {
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i;
return regex.test(str);
}
export function getCharaFilename() {
const context = getContext();
const fileName = context.characters[context.characterId].avatar;
if (fileName) {
return fileName.replace(/\.[^/.]+$/, "")
}
}
export function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
}

@@ -11,2 +11,3 @@ import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders } from "../script.js";

world_info_case_sensitive,
world_info_match_whole_words,
world_names,

@@ -29,5 +30,6 @@ imported_world_name,

let world_info_case_sensitive = false;
let world_info_match_whole_words = false;
let imported_world_name = "";
const saveWorldDebounced = debounce(async () => await _save(), 500);
const saveSettingsDebounced = debounce(() => saveSettings(), 500);
const saveWorldDebounced = debounce(async () => await _save(), 1000);
const saveSettingsDebounced = debounce(() => saveSettings(), 1000);

@@ -60,2 +62,4 @@ const world_info_position = {

world_info_case_sensitive = Boolean(settings.world_info_case_sensitive);
if (settings.world_info_match_whole_words !== undefined)
world_info_match_whole_words = Boolean(settings.world_info_match_whole_words);

@@ -70,2 +74,3 @@ $("#world_info_depth_counter").text(world_info_depth);

$("#world_info_case_sensitive").prop('checked', world_info_case_sensitive);
$("#world_info_match_whole_words").prop('checked', world_info_match_whole_words);

@@ -217,2 +222,10 @@ world_names = data.world_names?.length ? data.world_names : [];

// content
const countTokensDebounced = debounce(function (that, value) {
const numberOfTokens = getTokenCount(value);
$(that)
.closest(".world_entry")
.find(".world_entry_form_token_counter")
.text(numberOfTokens);
}, 1000);
const contentInput = template.find('textarea[name="content"]');

@@ -227,7 +240,3 @@ contentInput.data("uid", entry.uid);

// count tokens
const numberOfTokens = getTokenCount(value);
$(this)
.closest(".world_entry")
.find(".world_entry_form_token_counter")
.html(numberOfTokens);
countTokensDebounced(this, value);
});

@@ -332,2 +341,15 @@ contentInput.val(entry.content).trigger("input");

const excludeRecursionInput = template.find('input[name="exclude_recursion"]');
excludeRecursionInput.data("uid", entry.uid);
excludeRecursionInput.on("input", function () {
const uid = $(this).data("uid");
const value = $(this).prop("checked");
world_info_data.entries[uid].excludeRecursion = value;
saveWorldInfo();
});
excludeRecursionInput.prop("checked", entry.excludeRecursion).trigger("input");
excludeRecursionInput.siblings(".checkbox_fancy").click(function () {
$(this).siblings("input").click();
});
// delete button

@@ -367,2 +389,3 @@ const deleteButton = template.find("input.delete_entry_button");

disable: false,
excludeRecursion: false
};

@@ -507,2 +530,3 @@ const newUid = getFreeWorldEntryUid();

let needsToScan = true;
let count = 0;
let allActivatedEntries = new Set();

@@ -515,6 +539,9 @@

while (needsToScan) {
// Track how many times the loop has run
count++;
let activatedNow = new Set();
for (let entry of sortedEntries) {
if (allActivatedEntries.has(entry.uid) || entry.disable == true) {
if (allActivatedEntries.has(entry.uid) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) {
continue;

@@ -530,3 +557,3 @@ }

const substituted = substituteParams(key);
if (substituted && textToScan.includes(transformString(substituted.trim()))) {
if (substituted && matchKeys(textToScan, substituted.trim())) {
if (

@@ -539,6 +566,3 @@ entry.selective &&

const secondarySubstituted = substituteParams(keysecondary);
if (
secondarySubstituted &&
textToScan.includes(transformString(secondarySubstituted.trim()))
) {
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
activatedNow.add(entry.uid);

@@ -591,2 +615,25 @@ break secondary;

function matchKeys(haystack, needle) {
const transformedString = transformString(needle);
if (world_info_match_whole_words) {
const keyWords = transformedString.split(/\s+/);
if (keyWords.length > 1) {
return haystack.includes(transformedString);
}
else {
const regex = new RegExp(`\\b${transformedString}\\b`);
if (regex.test(haystack)) {
return true;
}
}
} else {
return haystack.includes(transformedString);
}
return false;
}
function selectImportedWorldInfo() {

@@ -715,2 +762,7 @@ if (!imported_world_name) {

})
$('#world_info_match_whole_words').on('input', function () {
world_info_match_whole_words = !!$(this).prop('checked');
saveSettingsDebounced();
});
});
How to Update SillyTavern
The most recent version can be found here: https://docs.sillytavern.app/usage/update/
This is not an installation guide. If you need installation instructions, look here:
https://docs.alpindale.dev/pygmalion-extras/sillytavern/#installation
https://docs.sillytavern.app/installation/windows/
This guide assumes you have already installed SillyTavern once, and know how to run it on your OS.
Linux/Termux:
Linux/Termux:
You definitely installed via git, so just 'git pull' inside the SillyTavern directory.
Windows/MacOS:
Windows/MacOS:
Method 1 - GIT
We always recommend users install using 'git'. Here's why:
We always recommend users install using 'git'. Here's why:
When you have installed via `git clone`, all you have to do to update is type `git pull` in a command line in the ST folder.
You can also try running the 'UpdateAndStart.bat' file, which will almost do the same thing. (Windows only)
When you have installed via `git clone`, all you have to do to update is type `git pull` in a command line in the ST folder.
You can also try running the 'UpdateAndStart.bat' file, which will almost do the same thing. (Windows only)
Alternatively, if the command prompt gives you problems (and you have GitHub Desktop installed), you can use the 'Repository' menu and select 'Pull'.

@@ -25,9 +27,9 @@ The updates are applied automatically and safely.

If you insist on installing via a zip, here is the tedious process for doing the update:
If you insist on installing via a zip, here is the tedious process for doing the update:
1. Download the new release zip.
2. Unzip it into a folder OUTSIDE of your current ST installation.
3. Do the usual setup procedure for your OS to install the NodeJS requirements.
3. Do the usual setup procedure for your OS to install the NodeJS requirements.
4. Copy the following files/folders as necessary(*) from your old ST installation:
4. Copy the following files/folders as necessary(*) from your old ST installation:

@@ -48,3 +50,3 @@ - Backgrounds

(*) 'As necessary' = "If you made any custom content related to those folders".
(*) 'As necessary' = "If you made any custom content related to those folders".
None of the folders are mandatory, so only copy what you need.

@@ -55,3 +57,3 @@

5. Paste those items into the /Public/ folder of the new install.
5. Paste those items into the /Public/ folder of the new install.

@@ -58,0 +60,0 @@ 6. Start SillyTavern once again with the method appropriate to your OS, and pray you got it right.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc