mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Copy extensions from extras project to main
This commit is contained in:
1
public/img/dice-solid.svg
Normal file
1
public/img/dice-solid.svg
Normal 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 |
1
public/img/image-solid.svg
Normal file
1
public/img/image-solid.svg
Normal 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 |
1
public/img/spinner-solid.svg
Normal file
1
public/img/spinner-solid.svg
Normal 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 |
@ -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;">
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -1,93 +1,62 @@
|
||||
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 = [];
|
||||
|
||||
(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 settings_style = `
|
||||
<style>
|
||||
#extensions_url {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#extensions_loaded {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
let modules = [];
|
||||
let activeExtensions = new Set();
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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>`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Could not activate extension: ${name}`);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function connectClickHandler() {
|
||||
const baseUrl = $("#extensions_url").val();
|
||||
localStorage.setItem(extensions_urlKey, baseUrl);
|
||||
@ -105,15 +74,15 @@ let extensions = [];
|
||||
|
||||
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);
|
||||
@ -129,89 +98,71 @@ let extensions = [];
|
||||
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();
|
||||
|
||||
for (let extension of extensions) {
|
||||
$('#extensions_list').append(`<li id="${extension.name}">${extension.metadata.display_name}</li>`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$('#extensions_loaded').hide(200);
|
||||
$('#extensions_settings').hide(200);
|
||||
$('#extensions_list').empty();
|
||||
}
|
||||
}
|
||||
|
||||
function applyExtensions(baseUrl) {
|
||||
const url = new URL(baseUrl);
|
||||
function addExtensionStyle(name, manifest) {
|
||||
if (manifest.css) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `/scripts/extensions/${name}/${manifest.css}`;
|
||||
|
||||
if (!Array.isArray(extensions) || extensions.length === 0) {
|
||||
return;
|
||||
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);
|
||||
}
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
addExtensionStyle(extension);
|
||||
addExtensionScript(extension);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function addExtensionStyle(extension) {
|
||||
if (extension.metadata.css) {
|
||||
try {
|
||||
url.pathname = `/api/style/${extension.name}`;
|
||||
const link = url.toString();
|
||||
function addExtensionScript(name, manifest) {
|
||||
if (manifest.js) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `/scripts/extensions/${name}/${manifest.js}`;
|
||||
let ready = false;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
})();
|
122
public/scripts/extensions/caption/index.js
Normal file
122
public/scripts/extensions/caption/index.js
Normal 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);
|
||||
});
|
9
public/scripts/extensions/caption/manifest.json
Normal file
9
public/scripts/extensions/caption/manifest.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"display_name": "Image Captioning",
|
||||
"loading_order": 4,
|
||||
"requires": [
|
||||
"caption"
|
||||
],
|
||||
"js": "index.js",
|
||||
"css": "style.css"
|
||||
}
|
36
public/scripts/extensions/caption/style.css
Normal file
36
public/scripts/extensions/caption/style.css
Normal 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);}
|
||||
}
|
108
public/scripts/extensions/dice/droll.js
Normal file
108
public/scripts/extensions/dice/droll.js
Normal 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
|
95
public/scripts/extensions/dice/index.js
Normal file
95
public/scripts/extensions/dice/index.js
Normal 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);
|
||||
});
|
7
public/scripts/extensions/dice/manifest.json
Normal file
7
public/scripts/extensions/dice/manifest.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"display_name": "D&D Dice",
|
||||
"loading_order": 5,
|
||||
"requires": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css"
|
||||
}
|
40
public/scripts/extensions/dice/style.css
Normal file
40
public/scripts/extensions/dice/style.css
Normal 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;
|
||||
}
|
218
public/scripts/extensions/expressions/index.js
Normal file
218
public/scripts/extensions/expressions/index.js
Normal 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);
|
||||
})();
|
9
public/scripts/extensions/expressions/manifest.json
Normal file
9
public/scripts/extensions/expressions/manifest.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"display_name": "Character Expressions",
|
||||
"loading_order": 6,
|
||||
"requires": [
|
||||
"classify"
|
||||
],
|
||||
"js": "index.js",
|
||||
"css": "style.css"
|
||||
}
|
63
public/scripts/extensions/expressions/style.css
Normal file
63
public/scripts/extensions/expressions/style.css
Normal 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;
|
||||
}
|
||||
}
|
72
public/scripts/extensions/floating-prompt/index.js
Normal file
72
public/scripts/extensions/floating-prompt/index.js
Normal 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);
|
||||
})();
|
7
public/scripts/extensions/floating-prompt/manifest.json
Normal file
7
public/scripts/extensions/floating-prompt/manifest.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"display_name": "Floating Prompt",
|
||||
"loading_order": 1,
|
||||
"requires": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css"
|
||||
}
|
9
public/scripts/extensions/floating-prompt/style.css
Normal file
9
public/scripts/extensions/floating-prompt/style.css
Normal file
@ -0,0 +1,9 @@
|
||||
.floating_prompt_settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#extension_floating_counter {
|
||||
font-weight: 600;
|
||||
color: orange;
|
||||
}
|
357
public/scripts/extensions/memory/index.js
Normal file
357
public/scripts/extensions/memory/index.js
Normal 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);
|
||||
});
|
9
public/scripts/extensions/memory/manifest.json
Normal file
9
public/scripts/extensions/memory/manifest.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"display_name": "Memory",
|
||||
"loading_order": 9,
|
||||
"requires": [
|
||||
"summarize"
|
||||
],
|
||||
"js": "index.js",
|
||||
"css": "style.css"
|
||||
}
|
34
public/scripts/extensions/memory/style.css
Normal file
34
public/scripts/extensions/memory/style.css
Normal 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;
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
Reference in New Issue
Block a user