mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
An API key is extremely important for ST-Extras servers that are exposed to the internet. Add an API key field below where the user enters the extras URL. For convenience, this key is persisted whenever the user refreshes the page. Also modify the fetch requests to always include API keys if present. See ST-Extras for more information on how this works. Signed-off-by: kingbri <bdashore3@proton.me>
498 lines
15 KiB
JavaScript
498 lines
15 KiB
JavaScript
import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
|
|
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
|
|
export { MODULE_NAME };
|
|
|
|
const MODULE_NAME = 'expressions';
|
|
const UPDATE_INTERVAL = 2000;
|
|
const DEFAULT_EXPRESSIONS = [
|
|
"admiration",
|
|
"amusement",
|
|
"anger",
|
|
"annoyance",
|
|
"approval",
|
|
"caring",
|
|
"confusion",
|
|
"curiosity",
|
|
"desire",
|
|
"disappointment",
|
|
"disapproval",
|
|
"disgust",
|
|
"embarrassment",
|
|
"excitement",
|
|
"fear",
|
|
"gratitude",
|
|
"grief",
|
|
"joy",
|
|
"love",
|
|
"nervousness",
|
|
"optimism",
|
|
"pride",
|
|
"realization",
|
|
"relief",
|
|
"remorse",
|
|
"sadness",
|
|
"surprise",
|
|
"neutral"
|
|
];
|
|
|
|
let expressionsList = null;
|
|
let lastCharacter = undefined;
|
|
let lastMessage = null;
|
|
let spriteCache = {};
|
|
let inApiCall = false;
|
|
|
|
function onExpressionsShowDefaultInput() {
|
|
const value = $(this).prop('checked');
|
|
extension_settings.expressions.showDefault = value;
|
|
saveSettingsDebounced();
|
|
|
|
const existingImageSrc = $('img.expression').prop('src');
|
|
if (existingImageSrc !== undefined) { //if we have an image in src
|
|
if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default)
|
|
$('img.expression').prop('src', ''); //remove it
|
|
lastMessage = null;
|
|
}
|
|
if (value) {
|
|
lastMessage = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function moduleWorker() {
|
|
const context = getContext();
|
|
|
|
// non-characters not supported
|
|
if (!context.groupId && context.characterId === undefined) {
|
|
removeExpression();
|
|
return;
|
|
}
|
|
|
|
// character changed
|
|
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
|
|
removeExpression();
|
|
spriteCache = {};
|
|
}
|
|
|
|
const currentLastMessage = getLastCharacterMessage();
|
|
|
|
// character has no expressions or it is not loaded
|
|
if (Object.keys(spriteCache).length === 0) {
|
|
await validateImages(currentLastMessage.name);
|
|
lastCharacter = context.groupId || context.characterId;
|
|
}
|
|
|
|
const offlineMode = $('.expression_settings .offline_mode');
|
|
if (!modules.includes('classify')) {
|
|
$('.expression_settings').show();
|
|
offlineMode.css('display', 'block');
|
|
lastCharacter = context.groupId || context.characterId;
|
|
|
|
if (context.groupId) {
|
|
await validateImages(currentLastMessage.name, true);
|
|
}
|
|
|
|
return;
|
|
}
|
|
else {
|
|
// force reload expressions list on connect to API
|
|
if (offlineMode.is(':visible')) {
|
|
expressionsList = null;
|
|
spriteCache = {};
|
|
expressionsList = await getExpressionsList();
|
|
await validateImages(currentLastMessage.name, true);
|
|
}
|
|
|
|
offlineMode.css('display', 'none');
|
|
}
|
|
|
|
|
|
// check if last message changed
|
|
if ((lastCharacter === context.characterId || lastCharacter === context.groupId)
|
|
&& lastMessage === currentLastMessage.mes) {
|
|
return;
|
|
}
|
|
|
|
// API is busy
|
|
if (inApiCall) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
inApiCall = true;
|
|
const url = new URL(getApiUrl());
|
|
url.pathname = '/api/classify';
|
|
|
|
const apiResult = await doExtrasFetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Bypass-Tunnel-Reminder': 'bypass',
|
|
},
|
|
body: JSON.stringify({ text: currentLastMessage.mes })
|
|
});
|
|
|
|
if (apiResult.ok) {
|
|
const name = context.groupId ? currentLastMessage.name : context.name2;
|
|
const force = !!context.groupId;
|
|
const data = await apiResult.json();
|
|
let expression = data.classification[0].label;
|
|
|
|
// Character won't be angry on you for swiping
|
|
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
|
|
expression = 'joy';
|
|
}
|
|
|
|
setExpression(name, expression, force);
|
|
}
|
|
|
|
}
|
|
catch (error) {
|
|
console.log(error);
|
|
}
|
|
finally {
|
|
inApiCall = false;
|
|
lastCharacter = context.groupId || context.characterId;
|
|
lastMessage = currentLastMessage.mes;
|
|
}
|
|
}
|
|
|
|
function getLastCharacterMessage() {
|
|
const context = getContext();
|
|
const reversedChat = context.chat.slice().reverse();
|
|
|
|
for (let mes of reversedChat) {
|
|
if (mes.is_user || mes.is_system) {
|
|
continue;
|
|
}
|
|
|
|
return { mes: mes.mes, name: mes.name };
|
|
}
|
|
|
|
return { mes: '', name: null };
|
|
}
|
|
|
|
function removeExpression() {
|
|
lastMessage = null;
|
|
$('img.expression').off('error');
|
|
$('img.expression').prop('src', '');
|
|
$('img.expression').removeClass('default');
|
|
$('.expression_settings').hide();
|
|
}
|
|
|
|
async function validateImages(character, forceRedrawCached) {
|
|
if (!character) {
|
|
return;
|
|
}
|
|
|
|
const labels = await getExpressionsList();
|
|
|
|
if (spriteCache[character]) {
|
|
if (forceRedrawCached && $('#image_list').data('name') !== character) {
|
|
console.debug('force redrawing character sprites list')
|
|
drawSpritesList(character, labels, spriteCache[character]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const sprites = await getSpritesList(character);
|
|
let validExpressions = drawSpritesList(character, labels, sprites);
|
|
spriteCache[character] = validExpressions;
|
|
}
|
|
|
|
function drawSpritesList(character, labels, sprites) {
|
|
let validExpressions = [];
|
|
$('.expression_settings').show();
|
|
$('#image_list').empty();
|
|
$('#image_list').data('name', character);
|
|
labels.sort().forEach((item) => {
|
|
const sprite = sprites.find(x => x.label == item);
|
|
|
|
if (sprite) {
|
|
validExpressions.push(sprite);
|
|
$('#image_list').append(getListItem(item, sprite.path, 'success'));
|
|
}
|
|
else {
|
|
$('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure'));
|
|
}
|
|
});
|
|
return validExpressions;
|
|
}
|
|
|
|
function getListItem(item, imageSrc, textClass) {
|
|
return `
|
|
<div id="${item}" class="expression_list_item">
|
|
<div class="expression_list_buttons">
|
|
<div class="menu_button expression_list_upload" title="Upload image">
|
|
<i class="fa-solid fa-upload"></i>
|
|
</div>
|
|
<div class="menu_button expression_list_delete" title="Delete image">
|
|
<i class="fa-solid fa-trash"></i>
|
|
</div>
|
|
</div>
|
|
<span class="expression_list_title ${textClass}">${item}</span>
|
|
<img class="expression_list_image" src="${imageSrc}" />
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function getSpritesList(name) {
|
|
console.debug('getting sprites list');
|
|
|
|
try {
|
|
const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`);
|
|
|
|
let sprites = result.ok ? (await result.json()) : [];
|
|
return sprites;
|
|
}
|
|
catch (err) {
|
|
console.log(err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function getExpressionsList() {
|
|
// get something for offline mode (default images)
|
|
if (!modules.includes('classify')) {
|
|
return DEFAULT_EXPRESSIONS;
|
|
}
|
|
|
|
if (Array.isArray(expressionsList)) {
|
|
return expressionsList;
|
|
}
|
|
|
|
const url = new URL(getApiUrl());
|
|
url.pathname = '/api/classify/labels';
|
|
|
|
try {
|
|
const apiResult = await doExtrasFetch(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, force) {
|
|
console.debug('entered setExpressions');
|
|
await validateImages(character);
|
|
const img = $('img.expression');
|
|
|
|
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
|
console.debug('checking for expression images to show..');
|
|
if (sprite) {
|
|
console.debug('setting expression from character images folder');
|
|
img.attr('src', sprite.path);
|
|
img.removeClass('default');
|
|
img.off('error');
|
|
img.on('error', function () {
|
|
$(this).attr('src', '');
|
|
if (force && extension_settings.expressions.showDefault) {
|
|
setDefault();
|
|
}
|
|
});
|
|
} else {
|
|
if (extension_settings.expressions.showDefault) {
|
|
setDefault();
|
|
}
|
|
}
|
|
|
|
function setDefault() {
|
|
console.debug('setting default');
|
|
const defImgUrl = `/img/default-expressions/${expression}.png`;
|
|
//console.log(defImgUrl);
|
|
img.attr('src', defImgUrl);
|
|
img.addClass('default');
|
|
}
|
|
document.getElementById("expression-holder").style.display = '';
|
|
}
|
|
|
|
function onClickExpressionImage() {
|
|
// online mode doesn't need force set
|
|
if (modules.includes('classify')) {
|
|
return;
|
|
}
|
|
|
|
const expression = $(this).attr('id');
|
|
const name = getLastCharacterMessage().name;
|
|
|
|
if ($(this).find('.failure').length === 0) {
|
|
setExpression(name, expression, true);
|
|
}
|
|
}
|
|
async function handleFileUpload(url, formData) {
|
|
try {
|
|
const data = await jQuery.ajax({
|
|
type: "POST",
|
|
url: url,
|
|
data: formData,
|
|
beforeSend: function () { },
|
|
cache: false,
|
|
contentType: false,
|
|
processData: false,
|
|
});
|
|
|
|
// Refresh sprites list
|
|
const name = formData.get('name');
|
|
delete spriteCache[name];
|
|
await validateImages(name);
|
|
|
|
return data;
|
|
} catch (error) {
|
|
toastr.error('Failed to upload image');
|
|
}
|
|
}
|
|
|
|
async function onClickExpressionUpload(event) {
|
|
// Prevents the expression from being set
|
|
event.stopPropagation();
|
|
|
|
const id = $(this).closest('.expression_list_item').attr('id');
|
|
const name = $('#image_list').data('name');
|
|
|
|
const handleExpressionUploadChange = async (e) => {
|
|
const file = e.target.files[0];
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('name', name);
|
|
formData.append('label', id);
|
|
formData.append('avatar', file);
|
|
|
|
await handleFileUpload('/upload_sprite', formData);
|
|
|
|
// Reset the input
|
|
e.target.form.reset();
|
|
};
|
|
|
|
$('#expression_upload')
|
|
.off('change')
|
|
.on('change', handleExpressionUploadChange)
|
|
.trigger('click');
|
|
}
|
|
|
|
async function onClickExpressionUploadPackButton() {
|
|
const name = $('#image_list').data('name');
|
|
|
|
const handleFileUploadChange = async (e) => {
|
|
const file = e.target.files[0];
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('name', name);
|
|
formData.append('avatar', file);
|
|
|
|
const { count } = await handleFileUpload('/upload_sprite_pack', formData);
|
|
toastr.success(`Uploaded ${count} image(s) for ${name}`);
|
|
|
|
// Reset the input
|
|
e.target.form.reset();
|
|
};
|
|
|
|
$('#expression_upload_pack')
|
|
.off('change')
|
|
.on('change', handleFileUploadChange)
|
|
.trigger('click');
|
|
}
|
|
|
|
async function onClickExpressionDelete(event) {
|
|
// Prevents the expression from being set
|
|
event.stopPropagation();
|
|
|
|
const confirmation = await callPopup("<h3>Are you sure?</h3>Once deleted, it's gone forever!", 'confirm');
|
|
|
|
if (!confirmation) {
|
|
return;
|
|
}
|
|
|
|
const id = $(this).closest('.expression_list_item').attr('id');
|
|
const name = $('#image_list').data('name');
|
|
|
|
try {
|
|
await fetch('/delete_sprite', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify({ name, label: id }),
|
|
});
|
|
} catch (error) {
|
|
toastr.error('Failed to delete image. Try again later.');
|
|
}
|
|
|
|
// Refresh sprites list
|
|
delete spriteCache[name];
|
|
await validateImages(name);
|
|
}
|
|
|
|
(function () {
|
|
function addExpressionImage() {
|
|
const html = `
|
|
<div id="expression-wrapper">
|
|
<div id="expression-holder" class="expression-holder" style="display:none;">
|
|
<div id="expression-holderheader" class="fa-solid fa-grip drag-grabber"></div>
|
|
<img id="expression-image" class="expression">
|
|
</div>
|
|
</div>`;
|
|
$('body').append(html);
|
|
}
|
|
function addSettings() {
|
|
|
|
const html = `
|
|
<div class="expression_settings">
|
|
<div class="inline-drawer">
|
|
<div class="inline-drawer-toggle inline-drawer-header">
|
|
<b>Expression images</b>
|
|
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
|
</div>
|
|
<div class="inline-drawer-content">
|
|
<p class="offline_mode">You are in offline mode. Click on the image below to set the expression.</p>
|
|
<div id="image_list"></div>
|
|
<div class="expression_buttons">
|
|
<div id="expression_upload_pack_button" class="menu_button">
|
|
<i class="fa-solid fa-file-zipper"></i>
|
|
<span>Upload sprite pack (ZIP)</span>
|
|
</div>
|
|
</div>
|
|
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.
|
|
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
|
|
<label for="expressions_show_default"><input id="expressions_show_default" type="checkbox">Show default images (emojis) if missing</label>
|
|
</div>
|
|
</div>
|
|
<form>
|
|
<input type="file" id="expression_upload_pack" name="expression_upload_pack" accept="application/zip" hidden>
|
|
<input type="file" id="expression_upload" name="expression_upload" accept="image/*" hidden>
|
|
</form>
|
|
</div>
|
|
`;
|
|
$('#extensions_settings').append(html);
|
|
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
|
|
$('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);
|
|
$('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
|
|
$(document).on('click', '.expression_list_item', onClickExpressionImage);
|
|
$(document).on('click', '.expression_list_upload', onClickExpressionUpload);
|
|
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
|
|
$('.expression_settings').hide();
|
|
}
|
|
|
|
addExpressionImage();
|
|
addSettings();
|
|
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
|
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
|
|
moduleWorker();
|
|
})();
|