Copy extensions from extras project to main

This commit is contained in:
SillyLossy
2023-03-16 00:33:26 +02:00
parent 2cda80ffe4
commit 165cad1549
24 changed files with 1423 additions and 198 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M252.3 11.7c-15.6-15.6-40.9-15.6-56.6 0l-184 184c-15.6 15.6-15.6 40.9 0 56.6l184 184c15.6 15.6 40.9 15.6 56.6 0l184-184c15.6-15.6 15.6-40.9 0-56.6l-184-184zM248 224c0 13.3-10.7 24-24 24s-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24zM96 248c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24zm128 80c13.3 0 24 10.7 24 24s-10.7 24-24 24s-24-10.7-24-24s10.7-24 24-24zm128-80c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24zM224 72c13.3 0 24 10.7 24 24s-10.7 24-24 24s-24-10.7-24-24s10.7-24 24-24zm96 392c0 26.5 21.5 48 48 48H592c26.5 0 48-21.5 48-48V240c0-26.5-21.5-48-48-48H472.5c13.4 26.9 8.8 60.5-13.6 82.9L320 413.8V464zm160-88c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24z"/></svg>

After

Width:  |  Height:  |  Size: 970 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6h96 32H424c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>

After

Width:  |  Height:  |  Size: 634 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M304 48c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zm0 416c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM48 304c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48zm464-48c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM142.9 437c18.7-18.7 18.7-49.1 0-67.9s-49.1-18.7-67.9 0s-18.7 49.1 0 67.9s49.1 18.7 67.9 0zm0-294.2c18.7-18.7 18.7-49.1 0-67.9S93.7 56.2 75 75s-18.7 49.1 0 67.9s49.1 18.7 67.9 0zM369.1 437c18.7 18.7 49.1 18.7 67.9 0s18.7-49.1 0-67.9s-49.1-18.7-67.9 0s-18.7 49.1 0 67.9z"/></svg>

After

Width:  |  Height:  |  Size: 805 B

View File

@ -747,6 +747,25 @@
</div>
</div>
<div class="extensions_block">
<hr>
<h3>Extensions: <a target="_blank" href="https://github.com/SillyLossy/TavernAI-extras">TavernAI-extras</a></h3>
<input id="extensions_url" type="text" class="text_pole" />
<div class="extensions_url_block">
<input id="extensions_connect" class="menu_button" type="submit" value="Connect" />
<span class="expander"></span>
<label for="extensions_autoconnect"><input id="extensions_autoconnect" type="checkbox"/>Auto-connect</label>
</div>
<div id="extensions_status">Not connected</div>
<div id="extensions_loaded">
<h4>Active extensions</h4>
<ul id="extensions_list">
</ul>
</div>
<div id="extensions_settings">
<h3>Extension settings</h3>
</div>
</div>
</div>
<div id="rm_character_import" class="right_menu" style="display: none;">

View File

@ -2163,7 +2163,7 @@ async function getSettings(type) {
const src = "scripts/extensions.js";
if ($(`script[src="${src}"]`).length === 0) {
const script = document.createElement("script");
script.type = "text/javascript";
script.type = "module";
script.src = src;
$("body").append(script);
}

View File

@ -1,119 +1,88 @@
import { isSubsetOf } from "./utils.js";
export {
getContext,
getApiUrl,
defaultRequestArgs,
};
import * as captionManifest from "./extensions/caption/manifest.json" assert {type: 'json'};
import * as diceManifest from "./extensions/dice/manifest.json" assert {type: 'json'};
import * as expressionsManifest from "./extensions/expressions/manifest.json" assert {type: 'json'};
import * as floatingPromptManifest from "./extensions/floating-prompt/manifest.json" assert {type: 'json'};
import * as memoryManifest from "./extensions/memory/manifest.json" assert {type: 'json'};
const manifests = {
'floating-prompt': floatingPromptManifest.default,
'dice': diceManifest.default,
'caption': captionManifest.default,
'expressions': expressionsManifest.default,
'memory': memoryManifest.default,
};
const extensions_urlKey = 'extensions_url';
const extensions_autoConnectKey = 'extensions_autoconnect';
let extensions = [];
let modules = [];
let activeExtensions = new Set();
(function () {
const settings_html = `
<div class="extensions_block">
<hr>
<h3>Extensions: <a target="_blank" href="https://github.com/SillyLossy/TavernAI-extras">TavernAI-extras</a></h3>
<input id="extensions_url" type="text" class="text_pole" />
<div class="extensions_url_block">
<input id="extensions_connect" class="menu_button" type="submit" value="Connect" />
<span class="expander"></span>
<label for="extensions_autoconnect"><input id="extensions_autoconnect" type="checkbox"/>Auto-connect</label>
</div>
<div id="extensions_status">Not connected</div>
<div id="extensions_loaded">
<h4>Active extensions</h4>
<ul id="extensions_list">
</ul>
</div>
<div id="extensions_settings">
<h3>Extension settings</h3>
</div>
</div>
`;
const getContext = () => window['TavernAI'].getContext();
const getApiUrl = () => localStorage.getItem('extensions_url');
const defaultUrl = "http://localhost:5100";
const defaultRequestArgs = { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } };
let connectedToApi = false;
const settings_style = `
<style>
#extensions_url {
display: block;
async function activateExtensions() {
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
for (let entry of extensions) {
const name = entry[0];
const manifest = entry[1];
if (activeExtensions.has(name)) {
continue;
}
#extensions_loaded {
display: none;
// all required modules are active (offline extensions require none)
if (isSubsetOf(modules, manifest.requires)) {
try {
await addExtensionScript(name, manifest);
await addExtensionStyle(name, manifest);
activeExtensions.add(name);
$('#extensions_list').append(`<li id="${name}">${manifest.display_name}</li>`);
}
#extensions_status {
margin-bottom: 10px;
font-weight: 700;
catch (error) {
console.error(`Could not activate extension: ${name}`);
console.error(error);
}
.extensions_block input[type="submit"]:hover{
background-color: green;
}
.extensions_block input[type="checkbox"] {
margin-left: 10px;
margin-right: 10px;
}
}
label[for="extensions_autoconnect"] {
display: flex;
align-items: center;
margin: 0 !important;
}
.extensions_url_block {
display: flex;
align-items: center;
justify-content: space-between;
}
.extensions_url_block h4 {
display: inline;
}
.extensions_block {
clear: both;
padding: 0.05px; /* clear fix */
}
.success {
color: green;
}
.failure {
color: red;
}
.expander {
flex-grow: 1;
}
</style>
`;
const defaultUrl = "http://localhost:5100";
let connectedToApi = false;
async function connectClickHandler() {
async function connectClickHandler() {
const baseUrl = $("#extensions_url").val();
localStorage.setItem(extensions_urlKey, baseUrl);
await connectToApi(baseUrl);
}
}
function autoConnectInputHandler() {
function autoConnectInputHandler() {
const value = $(this).prop('checked');
localStorage.setItem(extensions_autoConnectKey, value.toString());
if (value && !connectedToApi) {
$("#extensions_connect").trigger('click');
}
}
}
async function connectToApi(baseUrl) {
async function connectToApi(baseUrl) {
const url = new URL(baseUrl);
url.pathname = '/api/extensions';
url.pathname = '/api/modules';
try {
const getExtensionsResult = await fetch(url, { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } });
const getExtensionsResult = await fetch(url, defaultRequestArgs);
if (getExtensionsResult.ok) {
const data = await getExtensionsResult.json();
extensions = data.extensions;
applyExtensions(baseUrl);
modules = data.modules;
activateExtensions();
}
updateStatus(getExtensionsResult.ok);
@ -121,97 +90,79 @@ let extensions = [];
catch {
updateStatus(false);
}
}
}
function updateStatus(success) {
function updateStatus(success) {
connectedToApi = success;
const _text = success ? 'Connected to API' : 'Could not connect to API';
const _class = success ? 'success' : 'failure';
$('#extensions_status').text(_text);
$('#extensions_status').attr('class', _class);
}
if (success && extensions.length) {
$('#extensions_loaded').show(200);
$('#extensions_settings').show(200);
$('#extensions_list').empty();
function addExtensionStyle(name, manifest) {
if (manifest.css) {
return new Promise((resolve, reject) => {
const url = `/scripts/extensions/${name}/${manifest.css}`;
for (let extension of extensions) {
$('#extensions_list').append(`<li id="${extension.name}">${extension.metadata.display_name}</li>`);
if ($(`link[id="${name}"]`).length === 0) {
const link = document.createElement('link');
link.id = name;
link.rel = "stylesheet";
link.type = "text/css";
link.href = url;
link.onload = function () {
resolve();
}
link.onerror = function (e) {
reject(e);
}
else {
$('#extensions_loaded').hide(200);
$('#extensions_settings').hide(200);
$('#extensions_list').empty();
document.head.appendChild(link);
}
});
}
function applyExtensions(baseUrl) {
const url = new URL(baseUrl);
return Promise.resolve();
}
if (!Array.isArray(extensions) || extensions.length === 0) {
return;
}
function addExtensionScript(name, manifest) {
if (manifest.js) {
return new Promise((resolve, reject) => {
const url = `/scripts/extensions/${name}/${manifest.js}`;
let ready = false;
for (let extension of extensions) {
addExtensionStyle(extension);
addExtensionScript(extension);
}
async function addExtensionStyle(extension) {
if (extension.metadata.css) {
try {
url.pathname = `/api/style/${extension.name}`;
const link = url.toString();
const result = await fetch(link, { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } });
const text = await result.text();
if ($(`style[id="${link}"]`).length === 0) {
const style = document.createElement('style');
style.id = link;
style.innerHTML = text;
$('head').append(style);
}
}
catch (error) {
console.log(error);
}
}
}
async function addExtensionScript(extension) {
if (extension.metadata.js) {
try {
url.pathname = `/api/script/${extension.name}`;
const link = url.toString();
const result = await fetch(link, { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } });
const text = await result.text();
if ($(`script[id="${link}"]`).length === 0) {
if ($(`script[id="${name}"]`).length === 0) {
const script = document.createElement('script');
script.id = link;
script.id = name;
script.type = 'module';
script.innerHTML = text;
$('body').append(script);
}
}
catch (error) {
console.log(error);
}
script.src = url;
script.async = true;
script.onerror = function (err) {
reject(err, script);
};
script.onload = script.onreadystatechange = function () {
// console.log(this.readyState); // uncomment this line to see which ready states are called.
if (!ready && (!this.readyState || this.readyState == 'complete')) {
ready = true;
resolve();
}
};
document.body.appendChild(script);
}
});
}
$(document).ready(async function () {
return Promise.resolve();
}
$(document).ready(async function () {
const url = localStorage.getItem(extensions_urlKey) ?? defaultUrl;
const autoConnect = localStorage.getItem(extensions_autoConnectKey) == 'true';
$('#rm_api_block').append(settings_html);
$('head').append(settings_style);
$("#extensions_url").val(url);
$("#extensions_connect").on('click', connectClickHandler);
$("#extensions_autoconnect").on('input', autoConnectInputHandler);
$("#extensions_autoconnect").prop('checked', autoConnect).trigger('input');
});
})();
// Activate offline extensions
activateExtensions();
});

View File

@ -0,0 +1,122 @@
import { getBase64Async } from "../../utils.js";
import { getContext, getApiUrl } from "../../extensions.js";
export { MODULE_NAME };
const MODULE_NAME = 'caption';
const UPDATE_INTERVAL = 1000;
async function moduleWorker() {
const context = getContext();
context.onlineStatus === 'no_connection'
? $('#send_picture').hide(200)
: $('#send_picture').show(200);
}
async function setImageIcon() {
try {
const sendButton = document.getElementById('send_picture');
sendButton.style.backgroundImage = `url('/img/image-solid.svg')`;
sendButton.classList.remove('spin');
}
catch (error) {
console.log(error);
}
}
async function setSpinnerIcon() {
try {
const sendButton = document.getElementById('send_picture');
sendButton.style.backgroundImage = `url('/img/spinner-solid.svg')`;
sendButton.classList.add('spin');
}
catch (error) {
console.log(error);
}
}
async function sendCaptionedMessage(caption, image) {
const context = getContext();
const messageText = `[${context.name1} sends ${context.name2 ?? ''} a picture that contains: ${caption}]`;
const message = {
name: context.name1,
is_user: true,
is_name: true,
send_date: Date.now(),
mes: messageText,
extra: { image: image },
};
context.chat.push(message);
context.addOneMessage(message);
await context.generate();
}
async function onSelectImage(e) {
setSpinnerIcon();
const file = e.target.files[0];
if (!file) {
return;
}
try {
const base64Img = await getBase64Async(file);
const url = new URL(getApiUrl());
url.pathname = '/api/caption';
const apiResult = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ image: base64Img.split(',')[1] })
});
if (apiResult.ok) {
const data = await apiResult.json();
const caption = data.caption;
await sendCaptionedMessage(caption, base64Img);
}
}
catch (error) {
console.log(error);
}
finally {
e.target.form.reset();
setImageIcon();
}
}
$(document).ready(function () {
function patchSendForm() {
const columns = $('#send_form').css('grid-template-columns').split(' ');
columns[columns.length - 1] = `${parseInt(columns[columns.length - 1]) + 40}px`;
columns[1] = 'auto';
$('#send_form').css('grid-template-columns', columns.join(' '));
}
function addSendPictureButton() {
const sendButton = document.createElement('input');
sendButton.type = 'button';
sendButton.id = 'send_picture';
$(sendButton).hide();
$(sendButton).on('click', () => $('#img_file').click());
$('#send_but_sheld').prepend(sendButton);
}
function addPictureSendForm() {
const inputHtml = `<input id="img_file" type="file" accept="image/*">`;
const imgForm = document.createElement('form');
imgForm.id = 'img_form';
$(imgForm).append(inputHtml);
$(imgForm).hide();
$('#form_sheld').append(imgForm);
$('#img_file').on('change', onSelectImage);
}
addPictureSendForm();
addSendPictureButton();
setImageIcon();
patchSendForm();
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
});

View File

@ -0,0 +1,9 @@
{
"display_name": "Image Captioning",
"loading_order": 4,
"requires": [
"caption"
],
"js": "index.js",
"css": "style.css"
}

View File

@ -0,0 +1,36 @@
#send_picture {
order: 200;
width: 40px;
height: 40px;
margin: 0;
padding: 1px;
background: no-repeat;
background-size: 26px auto;
background-position: center center;
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
filter: invert(1);
opacity: 0.5;
}
#send_picture:hover {
opacity: 1;
}
#img_form {
display: none;
}
.spin {
animation-name: spin;
animation-duration: 2000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes spin {
from {transform:rotate(0deg);}
to {transform:rotate(360deg);}
}

View File

@ -0,0 +1,108 @@
// Borrowed from the Droll library by thebinarypenguin
// https://github.com/thebinarypenguin/droll
// Licensed under MIT license
var droll = {};
// Define a "class" to represent a formula
function DrollFormula() {
this.numDice = 0;
this.numSides = 0;
this.modifier = 0;
this.minResult = 0;
this.maxResult = 0;
this.avgResult = 0;
}
// Define a "class" to represent the results of the roll
function DrollResult() {
this.rolls = [];
this.modifier = 0;
this.total = 0;
}
/**
* Returns a string representation of the roll result
*/
DrollResult.prototype.toString = function () {
if (this.rolls.length === 1 && this.modifier === 0) {
return this.rolls[0] + '';
}
if (this.rolls.length > 1 && this.modifier === 0) {
return this.rolls.join(' + ') + ' = ' + this.total;
}
if (this.rolls.length === 1 && this.modifier > 0) {
return this.rolls[0] + ' + ' + this.modifier + ' = ' + this.total;
}
if (this.rolls.length > 1 && this.modifier > 0) {
return this.rolls.join(' + ') + ' + ' + this.modifier + ' = ' + this.total;
}
if (this.rolls.length === 1 && this.modifier < 0) {
return this.rolls[0] + ' - ' + Math.abs(this.modifier) + ' = ' + this.total;
}
if (this.rolls.length > 1 && this.modifier < 0) {
return this.rolls.join(' + ') + ' - ' + Math.abs(this.modifier) + ' = ' + this.total;
}
};
/**
* Parse the formula into its component pieces.
* Returns a DrollFormula object on success or false on failure.
*/
droll.parse = function (formula) {
var pieces = null;
var result = new DrollFormula();
pieces = formula.match(/^([1-9]\d*)?d([1-9]\d*)([+-]\d+)?$/i);
if (!pieces) { return false; }
result.numDice = (pieces[1] - 0) || 1;
result.numSides = (pieces[2] - 0);
result.modifier = (pieces[3] - 0) || 0;
result.minResult = (result.numDice * 1) + result.modifier;
result.maxResult = (result.numDice * result.numSides) + result.modifier;
result.avgResult = (result.maxResult + result.minResult) / 2;
return result;
};
/**
* Test the validity of the formula.
* Returns true on success or false on failure.
*/
droll.validate = function (formula) {
return (droll.parse(formula)) ? true : false;
};
/**
* Roll the dice defined by the formula.
* Returns a DrollResult object on success or false on failure.
*/
droll.roll = function (formula) {
var pieces = null;
var result = new DrollResult();
pieces = droll.parse(formula);
if (!pieces) { return false; }
for (var a = 0; a < pieces.numDice; a++) {
result.rolls[a] = (1 + Math.floor(Math.random() * pieces.numSides));
}
result.modifier = pieces.modifier;
for (var b = 0; b < result.rolls.length; b++) {
result.total += result.rolls[b];
}
result.total += result.modifier;
return result;
};
// END OF DROLL CODE

View File

@ -0,0 +1,95 @@
import { getContext } from "../../extensions.js";
export { MODULE_NAME };
const MODULE_NAME = 'dice';
const UPDATE_INTERVAL = 1000;
function setDiceIcon() {
const sendButton = document.getElementById('roll_dice');
sendButton.style.backgroundImage = `url(/img/dice-solid.svg)`;
sendButton.classList.remove('spin');
}
function doDiceRoll() {
const value = $(this).data('value');
const isValid = droll.validate(value);
if (isValid) {
const result = droll.roll(value);
const context = getContext();
context.sendSystemMessage('generic', `${context.name1} rolls the ${value}. The result is: ${result.total}`);
}
}
function addDiceRollButton() {
const buttonHtml = `
<input id="roll_dice" type="button" />
<div id="dice_dropdown">
<ul class="list-group">
<li class="list-group-item" data-value="d4">d4</li>
<li class="list-group-item" data-value="d6">d6</li>
<li class="list-group-item" data-value="d8">d8</li>
<li class="list-group-item" data-value="d10">d10</li>
<li class="list-group-item" data-value="d12">d12</li>
<li class="list-group-item" data-value="d20">d20</li>
<li class="list-group-item" data-value="d100">d100</li>
</ul>
</div>
`;
$('#send_but_sheld').prepend(buttonHtml);
$('#dice_dropdown li').on('click', doDiceRoll);
const button = $('#roll_dice');
const dropdown = $('#dice_dropdown');
dropdown.hide();
button.hide();
let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
placement: 'top-start',
});
$(document).on('click touchend', function (e) {
const target = $(e.target);
if (target.is(dropdown)) return;
if (target.is(button) && !dropdown.is(":visible")) {
e.preventDefault();
dropdown.show();
popper.update();
} else {
dropdown.hide();
}
});
}
function addDiceScript() {
if (!window.droll) {
const script = document.createElement('script');
script.src = "/scripts/extensions/dice/droll.js";
document.body.appendChild(script);
}
}
function patchSendForm() {
const columns = $('#send_form').css('grid-template-columns').split(' ');
columns[columns.length - 1] = `${parseInt(columns[columns.length - 1]) + 40}px`;
columns[1] = 'auto';
$('#send_form').css('grid-template-columns', columns.join(' '));
}
async function moduleWorker() {
const context = getContext();
context.onlineStatus === 'no_connection'
? $('#roll_dice').hide(200)
: $('#roll_dice').show(200);
}
$(document).ready(function () {
addDiceScript();
addDiceRollButton();
patchSendForm();
setDiceIcon();
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
});

View File

@ -0,0 +1,7 @@
{
"display_name": "D&D Dice",
"loading_order": 5,
"requires": [],
"js": "index.js",
"css": "style.css"
}

View File

@ -0,0 +1,40 @@
#roll_dice {
order: 100;
width: 40px;
height: 40px;
margin: 0;
padding: 1px;
background: no-repeat;
background-size: 26px auto;
background-position: center center;
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
filter: invert(1);
opacity: 0.5;
}
#roll_dice:hover {
opacity: 1;
}
.list-group {
display: flex;
flex-direction: column;
padding-left: 0;
margin-top: 0;
margin-bottom: 3px;
}
.list-group-item {
position: relative;
display: block;
padding: 0.75rem 1.25rem;
margin-bottom: -1px;
background-color: rgba(0,0,0,0.3);
border: 1px solid rgba(0,0,0,0.7);
box-sizing: border-box;
user-select: none;
cursor: pointer;
}

View File

@ -0,0 +1,218 @@
import { getContext, getApiUrl } from "../../extensions.js";
import { urlContentToDataUri } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
const DEFAULT_KEY = 'extensions_expressions_showDefault';
const UPDATE_INTERVAL = 1000;
let expressionsList = null;
let lastCharacter = undefined;
let lastMessage = null;
let inApiCall = false;
let showDefault = false;
function loadSettings() {
showDefault = localStorage.getItem(DEFAULT_KEY) == 'true';
$('#expressions_show_default').prop('checked', showDefault).trigger('input');
}
function saveSettings() {
localStorage.setItem(DEFAULT_KEY, showDefault.toString());
}
function onExpressionsShowDefaultInput() {
const value = $(this).prop('checked');
showDefault = value;
saveSettings();
const existingImage = $('div.expression').css('background-image');
if (!value && existingImage.includes('data:image/png')) {
$('div.expression').css('background-image', 'unset');
lastMessage = null;
}
if (value) {
lastMessage = null;
}
}
async function moduleWorker() {
function getLastCharacterMessage() {
const reversedChat = context.chat.slice().reverse();
for (let mes of reversedChat) {
if (mes.is_user || mes.is_system) {
continue;
}
return mes.mes;
}
return '';
}
const context = getContext();
// group chats and non-characters not supported
if (context.groupId || !context.characterId) {
removeExpression();
return;
}
// character changed
if (lastCharacter !== context.characterId) {
removeExpression();
validateImages();
}
// check if last message changed
const currentLastMessage = getLastCharacterMessage();
if (lastCharacter === context.characterId && lastMessage === currentLastMessage) {
return;
}
// API is busy
if (inApiCall) {
return;
}
try {
inApiCall = true;
const url = new URL(getApiUrl());
url.pathname = '/api/classify';
const apiResult = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: currentLastMessage })
});
if (apiResult.ok) {
const data = await apiResult.json();
const expression = data.classification[0].label;
setExpression(context.name2, expression);
}
}
catch (error) {
console.log(error);
}
finally {
inApiCall = false;
lastCharacter = context.characterId;
lastMessage = currentLastMessage;
}
}
function removeExpression() {
lastMessage = null;
$('div.expression').css('background-image', 'unset');
$('.expression_settings').hide();
}
let imagesValidating = false;
async function validateImages() {
if (imagesValidating) {
return;
}
imagesValidating = true;
const context = getContext();
$('.expression_settings').show();
$('#image_list').empty();
if (!context.characterId) {
return;
}
const IMAGE_LIST = (await getExpressionsList()).map(x => `${x}.png`);
IMAGE_LIST.forEach((item) => {
const image = document.createElement('img');
image.src = `/characters/${context.name2}/${item}`;
image.classList.add('debug-image');
image.width = '0px';
image.height = '0px';
image.onload = function() {
$('#image_list').append(`<li id="${item}" class="success">${item} - OK</li>`);
}
image.onerror = function() {
$('#image_list').append(`<li id="${item}" class="failure">${item} - Missing</li>`);
}
$('#image_list').prepend(image);
});
imagesValidating = false;
}
async function getExpressionsList() {
if (Array.isArray(expressionsList)) {
return expressionsList;
}
const url = new URL(getApiUrl());
url.pathname = '/api/classify/labels';
try {
const apiResult = await fetch(url, {
method: 'GET',
headers: { 'Bypass-Tunnel-Reminder': 'bypass' },
});
if (apiResult.ok) {
const data = await apiResult.json();
expressionsList = data.labels;
return expressionsList;
}
}
catch (error) {
console.log(error);
return [];
}
}
async function setExpression(character, expression) {
const filename = `${expression}.png`;
const imgUrl = `url('/characters/${character}/${filename}')`;
$('div.expression').css('background-image', imgUrl);
const debugImageStatus = document.querySelector(`#image_list li[id="${filename}"]`);
if (showDefault && debugImageStatus && debugImageStatus.classList.contains('failure')) {
try {
const imgUrl = new URL(getApiUrl());
imgUrl.pathname = `/api/asset/${MODULE_NAME}/${filename}`;
const dataUri = await urlContentToDataUri(imgUrl.toString(), { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } });
$('div.expression').css('background-image', `url(${dataUri})`);
}
catch {
$('div.expression').css('background-image', 'unset');
}
}
}
(function () {
function addExpressionImage() {
const html = `<div class="expression"></div>`
$('body').append(html);
}
function addSettings() {
const html = `
<div class="expression_settings">
<h4>Expression images</h4>
<ul id="image_list"></ul>
<p><b>Hint:</b> <i>Create new folder in the <tt>public/characters/</tt> folder and name it as the name of the character. Put PNG images with expressions there.</i></p>
<label for="expressions_show_default"><input id="expressions_show_default" type="checkbox">Show default images (emojis) if missing</label>
</div>
`;
$('#extensions_settings').append(html);
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
$('.expression_settings').hide();
}
addExpressionImage();
addSettings();
loadSettings();
setInterval(moduleWorker, UPDATE_INTERVAL);
})();

View File

@ -0,0 +1,9 @@
{
"display_name": "Character Expressions",
"loading_order": 6,
"requires": [
"classify"
],
"js": "index.js",
"css": "style.css"
}

View File

@ -0,0 +1,63 @@
div.expression {
background-image: unset;
background-repeat: no-repeat;
background-size: contain;
background-position-y: bottom;
max-height: 90vh;
max-width: calc((100vw - 800px)/2);
width: 100%;
height: 100%;
position: fixed;
left: 0;
bottom: 0;
margin-left: 10px;
filter: drop-shadow(2px 2px 2px #51515199);
transition: 500ms;
}
.debug-image {
display: none;
visibility: collapse;
opacity: 0;
width: 0px;
height: 0px;
}
#image_list {
margin-top: 0;
margin-bottom: 0;
font-weight: 600;
}
#image_list .success {
color: green;
}
#image_list .failure {
color: red;
}
.expression_settings {
white-space: pre-line;
}
.expression_settings p {
margin-bottom: 0px;
}
.expression_settings label {
display: flex;
align-items: center;
flex-direction: row;
margin-left: 0px;
}
.expression_settings label input {
margin-left: 0px !important;
}
@media screen and (max-width:1200px) {
div.expression {
display: none;
}
}

View File

@ -0,0 +1,72 @@
import { getContext } from "../../extensions.js";
export { MODULE_NAME };
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
const PROMPT_KEY = 'extensions_floating_prompt';
const INTERVAL_KEY = 'extensions_floating_interval';
const UPDATE_INTERVAL = 1000;
let lastMessageNumber = null;
let promptInsertionInterval = 0;
function onExtensionFloatingPromptInput() {
saveSettings();
}
function onExtensionFloatingIntervalInput() {
promptInsertionInterval = Number($(this).val());
saveSettings();
}
function loadSettings() {
const prompt = localStorage.getItem(PROMPT_KEY);
const interval = localStorage.getItem(INTERVAL_KEY);
$('#extension_floating_prompt').val(prompt).trigger('input');
$('#extension_floating_interval').val(interval).trigger('input');
}
function saveSettings() {
localStorage.setItem(PROMPT_KEY, $('#extension_floating_prompt').val());
localStorage.setItem(INTERVAL_KEY, $('#extension_floating_interval').val());
}
async function moduleWorker() {
const context = getContext();
// take the count of messages
lastMessageNumber = Array.isArray(context.chat) && context.chat.length ? context.chat.filter(m => m.is_user).length : 0;
if (lastMessageNumber <= 0 || promptInsertionInterval <= 0) {
$('#extension_floating_counter').text('No');
return;
}
const messagesTillInsertion = (lastMessageNumber % promptInsertionInterval);
const shouldAddPrompt = messagesTillInsertion == 0;
const prompt = shouldAddPrompt ? $('#extension_floating_prompt').val() : '';
context.setExtensionPrompt(MODULE_NAME, prompt);
$('#extension_floating_counter').text(shouldAddPrompt ? 'This' : messagesTillInsertion);
}
(function() {
function addExtensionsSettings() {
const settingsHtml = `
<h4>Floating Prompt</h4>
<div class="floating_prompt_settings">
<label for="extension_floating_prompt">Append the following text to the scenario:</label>
<textarea id="extension_floating_prompt" class="text_pole" rows="2"></textarea>
<label for="extension_floating_interval">Every N messages <b>you</b> send (set to 0 to disable):</label>
<input id="extension_floating_interval" class="text_pole" type="number" value="0" min="0" max="999" />
<span>Appending the prompt in next: <span id="extension_floating_counter">No</span> message(s)</span>
</div>
`;
$('#extensions_settings').append(settingsHtml);
$('#extension_floating_prompt').on('input', onExtensionFloatingPromptInput);
$('#extension_floating_interval').on('input', onExtensionFloatingIntervalInput);
}
addExtensionsSettings();
loadSettings();
setInterval(moduleWorker, UPDATE_INTERVAL);
})();

View File

@ -0,0 +1,7 @@
{
"display_name": "Floating Prompt",
"loading_order": 1,
"requires": [],
"js": "index.js",
"css": "style.css"
}

View File

@ -0,0 +1,9 @@
.floating_prompt_settings {
display: flex;
flex-direction: column;
}
#extension_floating_counter {
font-weight: 600;
color: orange;
}

View File

@ -0,0 +1,357 @@
import { getStringHash, debounce } from "../../utils.js";
import { getContext, getApiUrl } from "../../extensions.js";
export { MODULE_NAME };
const MODULE_NAME = '1_memory';
const SETTINGS_KEY = 'extensions_memory_settings';
const UPDATE_INTERVAL = 1000;
let lastCharacterId = null;
let lastGroupId = null;
let lastChatId = null;
let lastMessageHash = null;
let lastMessageId = null;
let inApiCall = false;
const formatMemoryValue = (value) => value ? `[Context: "${value.trim()}"]` : '';
const saveChatDebounced = debounce(() => getContext().saveChat(), 2000);
const defaultSettings = {
minLongMemory: 16,
maxLongMemory: 512,
longMemoryLength: 128,
shortMemoryLength: 512,
minShortMemory: 128,
maxShortMemory: 2048,
shortMemoryStep: 16,
longMemoryStep: 8,
repetitionPenaltyStep: 0.05,
repetitionPenalty: 1.0,
maxRepetitionPenalty: 2.0,
minRepetitionPenalty: 1.0,
temperature: 1.0,
minTemperature: 0.1,
maxTemperature: 2.0,
temperatureStep: 0.05,
lengthPenalty: 1,
minLengthPenalty: 0,
maxLengthPenalty: 2,
lengthPenaltyStep: 0.05,
memoryFrozen: false,
};
const settings = {
shortMemoryLength: defaultSettings.shortMemoryLength,
longMemoryLength: defaultSettings.longMemoryLength,
repetitionPenalty: defaultSettings.repetitionPenalty,
temperature: defaultSettings.temperature,
lengthPenalty: defaultSettings.lengthPenalty,
memoryFrozen: defaultSettings.memoryFrozen,
}
function saveSettings() {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
}
function loadSettings() {
const savedSettings = JSON.parse(localStorage.getItem(SETTINGS_KEY));
if (savedSettings) {
Object.assign(settings, savedSettings);
$('#memory_long_length').val(settings.longMemoryLength).trigger('input');
$('#memory_short_length').val(settings.shortMemoryLength).trigger('input');
$('#memory_repetition_penalty').val(settings.repetitionPenalty).trigger('input');
$('#memory_temperature').val(settings.temperature).trigger('input');
$('#memory_length_penalty').val(settings.lengthPenalty).trigger('input');
$('#memory_frozen').prop('checked', settings.memoryFrozen).trigger('input');
}
}
function onMemoryShortInput() {
const value = $(this).val();
settings.shortMemoryLength = Number(value);
$('#memory_short_length_tokens').text(value);
saveSettings();
// Don't let long buffer be bigger than short
if (settings.longMemoryLength > settings.shortMemoryLength) {
$('#memory_long_length').val(settings.shortMemoryLength).trigger('input');
}
}
function onMemoryLongInput() {
const value = $(this).val();
settings.longMemoryLength = Number(value);
$('#memory_long_length_tokens').text(value);
saveSettings();
// Don't let long buffer be bigger than short
if (settings.longMemoryLength > settings.shortMemoryLength) {
$('#memory_short_length').val(settings.longMemoryLength).trigger('input');
}
}
function onMemoryRepetitionPenaltyInput() {
const value = $(this).val();
settings.repetitionPenalty = Number(value);
$('#memory_repetition_penalty_value').text(settings.repetitionPenalty.toFixed(2));
saveSettings();
}
function onMemoryTemperatureInput() {
const value = $(this).val();
settings.temperature = Number(value);
$('#memory_temperature_value').text(settings.temperature.toFixed(2));
saveSettings();
}
function onMemoryLengthPenaltyInput() {
const value = $(this).val();
settings.lengthPenalty = Number(value);
$('#memory_length_penalty_value').text(settings.lengthPenalty.toFixed(2));
saveSettings();
}
function onMemoryFrozenInput() {
const value = Boolean($(this).prop('checked'));
settings.memoryFrozen = value;
saveSettings();
}
function saveLastValues() {
const context = getContext();
lastGroupId = context.groupId;
lastCharacterId = context.characterId;
lastChatId = context.chatId;
lastMessageId = context.chat?.length ?? null;
lastMessageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1]['mes']) ?? '');
}
function getLatestMemoryFromChat(chat) {
if (!Array.isArray(chat) || !chat.length) {
return '';
}
const reversedChat = chat.slice().reverse();
for (let mes of reversedChat) {
if (mes.extra && mes.extra.memory) {
return mes.extra.memory;
}
}
return '';
}
async function moduleWorker() {
const context = getContext();
const chat = context.chat;
// no characters or group selected
if (!context.groupId && !context.characterId) {
return;
}
// Chat/character/group changed
if ((context.groupId && lastGroupId !== context.groupId) || (context.characterId !== lastCharacterId) || (context.chatId !== lastChatId)) {
const latestMemory = getLatestMemoryFromChat(chat);
setMemoryContext(latestMemory, false);
saveLastValues();
return;
}
// Currently summarizing or frozen state - skip
if (inApiCall || settings.memoryFrozen) {
return;
}
// No new messages - do nothing
if (lastMessageId === chat.length && getStringHash(chat[chat.length - 1].mes) === lastMessageHash) {
return;
}
// Messages has been deleted - rewrite the context with the latest available memory
if (chat.length < lastMessageId) {
const latestMemory = getLatestMemoryFromChat(chat);
setMemoryContext(latestMemory, false);
}
// Message has been edited / regenerated - delete the saved memory
if (chat.length
&& chat[chat.length - 1].extra
&& chat[chat.length - 1].extra.memory
&& lastMessageId === chat.length
&& getStringHash(chat[chat.length - 1].mes) !== lastMessageHash) {
delete chat[chat.length - 1].extra.memory;
}
try {
await summarizeChat(context);
}
catch (error) {
console.log(error);
}
finally {
saveLastValues();
}
}
async function summarizeChat(context) {
function getMemoryString() {
return (longMemory + '\n\n' + memoryBuffer.slice().reverse().join('\n\n')).trim();
}
const chat = context.chat;
const longMemory = getLatestMemoryFromChat(chat);
const reversedChat = chat.slice().reverse();
let memoryBuffer = [];
for (let mes of reversedChat) {
// we reached the point of latest memory
if (longMemory && mes.extra && mes.extra.memory == longMemory) {
break;
}
// don't care about system
if (mes.is_system) {
continue;
}
// determine the sender's name
const name = mes.is_user ? (context.name1 ?? 'You') : (mes.force_avatar ? mes.name : context.name2);
const entry = `${name}:\n${mes['mes']}`;
memoryBuffer.push(entry);
// check if token limit was reached
if (context.encode(getMemoryString()).length >= settings.shortMemoryLength) {
break;
}
}
const resultingString = getMemoryString();
if (context.encode(resultingString).length < settings.shortMemoryLength) {
return;
}
// perform the summarization API call
try {
inApiCall = true;
const url = new URL(getApiUrl());
url.pathname = '/api/summarize';
const apiResult = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({
text: resultingString,
params: {
min_length: settings.longMemoryLength * 0.8,
max_length: settings.longMemoryLength,
repetition_penalty: settings.repetitionPenalty,
temperature: settings.temperature,
length_penalty: settings.lengthPenalty,
}
})
});
if (apiResult.ok) {
const data = await apiResult.json();
const summary = data.summary;
const newContext = getContext();
// something changed during summarization request
if (newContext.groupId !== context.groupId || newContext.chatId !== context.chatId || (!newContext.groupId && (newContext.characterId !== context.characterId))) {
return;
}
setMemoryContext(summary, true);
}
}
catch (error) {
console.log(error);
}
finally {
inApiCall = false;
}
}
function onMemoryRestoreClick() {
const context = getContext();
const content = $('#memory_contents').val();
const reversedChat = context.chat.slice().reverse();
for (let mes of reversedChat) {
if (mes.extra && mes.extra.memory == content) {
delete mes.extra.memory;
break;
}
}
const newContent = getLatestMemoryFromChat(context.chat);
setMemoryContext(newContent, false);
}
function onMemoryContentInput() {
const value = $(this).val();
setMemoryContext(value, true);
}
function setMemoryContext(value, saveToMessage) {
const context = getContext();
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value));
$('#memory_contents').val(value);
if (saveToMessage && context.chat.length) {
const mes = context.chat[context.chat.length - 1];
if (!mes.extra) {
mes.extra = {};
}
mes.extra.memory = value;
saveChatDebounced();
}
}
$(document).ready(function () {
function addExtensionControls() {
const settingsHtml = `
<h4>Memory</h4>
<div id="memory_settings">
<label for="memory_contents">Memory contents</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>
</div>
<label for="memory_short_length">Memory summarization [short-term] 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">Memory context [long-term] 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}" />
<label for="memory_temperature">Summarization temperature (<span id="memory_temperature_value"></span>)</label>
<input id="memory_temperature" type="range" value="${defaultSettings.temperature}" min="${defaultSettings.minTemperature}" max="${defaultSettings.maxTemperature}" step="${defaultSettings.temperatureStep}" />
<label for="memory_repetition_penalty">Summarization repetition penalty (<span id="memory_repetition_penalty_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">Summarization length penalty (<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}" />
</div>
`;
$('#extensions_settings').append(settingsHtml);
$('#memory_restore').on('click', onMemoryRestoreClick);
$('#memory_contents').on('input', onMemoryContentInput);
$('#memory_long_length').on('input', onMemoryLongInput);
$('#memory_short_length').on('input', onMemoryShortInput);
$('#memory_repetition_penalty').on('input', onMemoryRepetitionPenaltyInput);
$('#memory_temperature').on('input', onMemoryTemperatureInput);
$('#memory_length_penalty').on('input', onMemoryLengthPenaltyInput);
$('#memory_frozen').on('input', onMemoryFrozenInput);
}
addExtensionControls();
loadSettings();
setInterval(moduleWorker, UPDATE_INTERVAL);
});

View File

@ -0,0 +1,9 @@
{
"display_name": "Memory",
"loading_order": 9,
"requires": [
"summarize"
],
"js": "index.js",
"css": "style.css"
}

View File

@ -0,0 +1,34 @@
#memory_settings {
display: flex;
flex-direction: column;
}
#memory_settings textarea {
font-size: 14px;
line-height: 1.2;
}
#memory_settings input[type="range"] {
margin-bottom: 20px;
}
#memory_settings label {
margin-bottom: 10px;
}
label[for="memory_frozen"] {
display: flex;
align-items: center;
margin: 0 !important;
}
label[for="memory_frozen"] input {
margin-right: 10px;
}
.memory_contents_controls {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}

View File

@ -7,6 +7,7 @@ export {
getStringHash,
debounce,
delay,
isSubsetOf,
};
/// UTILS
@ -84,3 +85,4 @@ function debounce(func, timeout = 300) {
}
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
const isSubsetOf = (a, b) => (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false;

View File

@ -216,6 +216,8 @@ code {
height: 40px;
position: relative;
background-position: center;
display: flex;
flex-direction: row;
}
#send_but {
@ -233,6 +235,7 @@ code {
cursor: pointer;
transition: 0.3s;
filter: brightness(0.5);
order: 99999;
}
#send_but:hover {
@ -2559,3 +2562,55 @@ a {
}
}
/* Extensions */
#extensions_url {
display: block;
}
#extensions_status {
margin-bottom: 10px;
font-weight: 700;
}
.extensions_block input[type="submit"]:hover{
background-color: green;
}
.extensions_block input[type="checkbox"] {
margin-left: 10px;
margin-right: 10px;
}
label[for="extensions_autoconnect"] {
display: flex;
align-items: center;
margin: 0 !important;
}
.extensions_url_block {
display: flex;
align-items: center;
justify-content: space-between;
}
.extensions_url_block h4 {
display: inline;
}
.extensions_block {
clear: both;
padding: 0.05px; /* clear fix */
}
.success {
color: green;
}
.failure {
color: red;
}
.expander {
flex-grow: 1;
}