Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging

This commit is contained in:
Cohee
2023-10-21 20:32:39 +03:00
14 changed files with 355 additions and 176 deletions

View File

@ -293,7 +293,7 @@
display: none; display: none;
} }
#bg_menu_content { .bg_list {
width: unset; width: unset;
} }
} }

View File

@ -138,8 +138,7 @@
filter: brightness(1); filter: brightness(1);
} }
.tags_view, .tags_view {
.open_alternate_greetings {
margin: 0; margin: 0;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
} }

View File

@ -3243,10 +3243,10 @@
Auto-select Auto-select
</div> </div>
</div> </div>
<h3 data-i18n="System Backgrounds" class="justifyCenter"> <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter">
System Backgrounds System Backgrounds
</h3> </h3>
<div id="bg_menu_content"> <div id="bg_menu_content" class="bg_list">
<form id="form_bg_download" class="bg_example no-border no-shadow" action="javascript:void(null);" method="post" enctype="multipart/form-data"> <form id="form_bg_download" class="bg_example no-border no-shadow" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<label class="input-file"> <label class="input-file">
<input type="file" id="add_bg_button" name="avatar" accept="image/png, image/jpeg, image/jpg, image/gif, image/bmp"> <input type="file" id="add_bg_button" name="avatar" accept="image/png, image/jpeg, image/jpg, image/gif, image/bmp">
@ -3254,6 +3254,14 @@
</label> </label>
</form> </form>
</div> </div>
<h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter">
Chat Backgrounds
</h3>
<div id="bg_chat_hint" class="wide100p textAlignCenter">
Chat backgrounds generated with the <code><i class="fa-solid fa-paintbrush"></i>&nbsp;Stable Diffusion</code> extension will appear here.
</div>
<div id="bg_custom_content" class="bg_list">
</div>
</div> </div>
</div> </div>
</div> </div>
@ -3519,8 +3527,10 @@
<span class="note-link-span">?</span> <span class="note-link-span">?</span>
</a> </a>
</span> </span>
<div class="menu_button open_alternate_greetings" title="Click to set additional greeting messages" data-i18n="[title]Click to set additional greeting messages"> <div class="menu_button menu_button_icon open_alternate_greetings margin0" title="Click to set additional greeting messages" data-i18n="[title]Click to set additional greeting messages">
<i class="fa-solid fa-comment-dots"></i> <span data-i18n="Alt. Greetings">
Alt. Greetings
</span>
</div> </div>
</div> </div>
<textarea id="firstmessage_textarea" data-i18n="[placeholder]This will be the first message from the character that starts every chat." placeholder="This will be the first message from the character that starts every chat." class="marginBot5" name="first_mes" placeholder=""></textarea> <textarea id="firstmessage_textarea" data-i18n="[placeholder]This will be the first message from the character that starts every chat." placeholder="This will be the first message from the character that starts every chat." class="marginBot5" name="first_mes" placeholder=""></textarea>
@ -3892,6 +3902,7 @@
<div id="background_template" class="template_element"> <div id="background_template" class="template_element">
<div class="bg_example flex-container" bgfile="" class="bg_example_img" title=""> <div class="bg_example flex-container" bgfile="" class="bg_example_img" title="">
<div title="Copy to system backgrounds" class="bg_button bg_example_copy fa-solid fa-file-arrow-up"></div>
<div title="Rename background" class="bg_button bg_example_edit fa-solid fa-pencil"></div> <div title="Rename background" class="bg_button bg_example_edit fa-solid fa-pencil"></div>
<div title="Lock" class="bg_button bg_example_lock fa-solid fa-lock"></div> <div title="Lock" class="bg_button bg_example_lock fa-solid fa-lock"></div>
<div title="Unlock" class="bg_button bg_example_unlock fa-solid fa-lock-open"></div> <div title="Unlock" class="bg_button bg_example_unlock fa-solid fa-lock-open"></div>

View File

@ -1675,7 +1675,7 @@ function substituteParams(content, _name1, _name2, _original, _group) {
if (typeof _original === 'string') { if (typeof _original === 'string') {
content = content.replace(/{{original}}/i, _original); content = content.replace(/{{original}}/i, _original);
} }
content = content.replace(/{{input}}/gi, $('#send_textarea').val()); content = content.replace(/{{input}}/gi, String($('#send_textarea').val()));
content = content.replace(/{{user}}/gi, _name1); content = content.replace(/{{user}}/gi, _name1);
content = content.replace(/{{char}}/gi, _name2); content = content.replace(/{{char}}/gi, _name2);
content = content.replace(/{{charIfNotGroup}}/gi, _group); content = content.replace(/{{charIfNotGroup}}/gi, _group);
@ -1686,11 +1686,14 @@ function substituteParams(content, _name1, _name2, _original, _group) {
content = content.replace(/<CHARIFNOTGROUP>/gi, _group); content = content.replace(/<CHARIFNOTGROUP>/gi, _group);
content = content.replace(/<GROUP>/gi, _group); content = content.replace(/<GROUP>/gi, _group);
content = content.replace(/\{\{\/\/(.*?)\}\}/g, "");
content = content.replace(/{{time}}/gi, moment().format('LT')); content = content.replace(/{{time}}/gi, moment().format('LT'));
content = content.replace(/{{date}}/gi, moment().format('LL')); content = content.replace(/{{date}}/gi, moment().format('LL'));
content = content.replace(/{{weekday}}/gi, moment().format('dddd')); content = content.replace(/{{weekday}}/gi, moment().format('dddd'));
content = content.replace(/{{isotime}}/gi, moment().format('HH:mm')); content = content.replace(/{{isotime}}/gi, moment().format('HH:mm'));
content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD')); content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD'));
content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => { content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => {
const formattedTime = moment().format(format); const formattedTime = moment().format(format);
return formattedTime; return formattedTime;
@ -7719,6 +7722,7 @@ jQuery(async function () {
$("#rm_button_selected_ch").children("h2").text(''); $("#rm_button_selected_ch").children("h2").text('');
select_rm_characters(); select_rm_characters();
sendSystemMessage(system_message_types.WELCOME); sendSystemMessage(system_message_types.WELCOME);
eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
} else { } else {
toastr.info("Please stop the message generation first."); toastr.info("Please stop the message generation first.");
} }
@ -8070,6 +8074,33 @@ jQuery(async function () {
}, 150); }, 150);
}) })
$(document).on("click", function (e) {
// Expanded options don't need to be closed
if (power_user.expand_message_actions) {
return;
}
// Check if the click was outside the relevant elements
if (!$(e.target).closest('.extraMesButtons, .extraMesButtonsHint').length) {
// Transition out the .extraMesButtons first
$('.extraMesButtons:visible').transition({
opacity: 0,
duration: 150,
easing: 'ease-in-out',
complete: function () {
$(this).hide(); // Hide the .extraMesButtons after the transition
// Transition the .extraMesButtonsHint back in
$('.extraMesButtonsHint:not(:visible)').show().transition({
opacity: .2,
duration: 150,
easing: 'ease-in-out'
});
}
});
}
});
$(document).on("click", ".mes_edit_cancel", function () { $(document).on("click", ".mes_edit_cancel", function () {
let text = chat[this_edit_mes_id]["mes"]; let text = chat[this_edit_mes_id]["mes"];

View File

@ -18,6 +18,8 @@ import {
getThumbnailUrl, getThumbnailUrl,
selectCharacterById, selectCharacterById,
eventSource, eventSource,
menu_type,
substituteParams,
} from "../script.js"; } from "../script.js";
import { import {
@ -234,7 +236,9 @@ export function RA_CountCharTokens() {
total_tokens += Number(counter.text()); total_tokens += Number(counter.text());
permanent_tokens += isPermanent ? Number(counter.text()) : 0; permanent_tokens += isPermanent ? Number(counter.text()) : 0;
} else { } else {
const tokens = getTokenCount(value); // We substitute macro for existing characters, but not for the character being created
const valueToCount = menu_type === 'create' ? value : substituteParams(value);
const tokens = getTokenCount(valueToCount);
counter.text(tokens); counter.text(tokens);
total_tokens += tokens; total_tokens += tokens;
permanent_tokens += isPermanent ? tokens : 0; permanent_tokens += isPermanent ? tokens : 0;

View File

@ -3,14 +3,24 @@ import { saveMetadataDebounced } from "./extensions.js";
import { registerSlashCommand } from "./slash-commands.js"; import { registerSlashCommand } from "./slash-commands.js";
import { stringFormat } from "./utils.js"; import { stringFormat } from "./utils.js";
const METADATA_KEY = 'custom_background'; const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
/** /**
* @param {string} background * Sets the background for the current chat and adds it to the list of custom backgrounds.
* @param {{url: string, path:string}} backgroundInfo
*/ */
function forceSetBackground(background) { function forceSetBackground(backgroundInfo) {
saveBackgroundMetadata(background); saveBackgroundMetadata(backgroundInfo.url);
setCustomBackground(); setCustomBackground();
const list = chat_metadata[LIST_METADATA_KEY] || [];
const bg = backgroundInfo.path;
list.push(bg);
chat_metadata[LIST_METADATA_KEY] = list;
saveMetadataDebounced();
getChatBackgroundsList();
highlightNewBackground(bg);
highlightLockedBackground(); highlightLockedBackground();
} }
@ -22,9 +32,27 @@ async function onChatChanged() {
unsetCustomBackground(); unsetCustomBackground();
} }
getChatBackgroundsList();
highlightLockedBackground(); highlightLockedBackground();
} }
function getChatBackgroundsList() {
const list = chat_metadata[LIST_METADATA_KEY];
const listEmpty = !Array.isArray(list) || list.length === 0;
$('#bg_custom_content').empty();
$('#bg_chat_hint').toggle(listEmpty);
if (listEmpty) {
return;
}
for (const bg of list) {
const template = getBackgroundFromTemplate(bg, true);
$('#bg_custom_content').append(template);
}
}
function getBackgroundPath(fileUrl) { function getBackgroundPath(fileUrl) {
return `backgrounds/${fileUrl}`; return `backgrounds/${fileUrl}`;
} }
@ -32,7 +60,7 @@ function getBackgroundPath(fileUrl) {
function highlightLockedBackground() { function highlightLockedBackground() {
$('.bg_example').removeClass('locked'); $('.bg_example').removeClass('locked');
const lockedBackground = chat_metadata[METADATA_KEY]; const lockedBackground = chat_metadata[BG_METADATA_KEY];
if (!lockedBackground) { if (!lockedBackground) {
return; return;
@ -71,21 +99,21 @@ function onUnlockBackgroundClick(e) {
} }
function hasCustomBackground() { function hasCustomBackground() {
return chat_metadata[METADATA_KEY]; return chat_metadata[BG_METADATA_KEY];
} }
function saveBackgroundMetadata(file) { function saveBackgroundMetadata(file) {
chat_metadata[METADATA_KEY] = file; chat_metadata[BG_METADATA_KEY] = file;
saveMetadataDebounced(); saveMetadataDebounced();
} }
function removeBackgroundMetadata() { function removeBackgroundMetadata() {
delete chat_metadata[METADATA_KEY]; delete chat_metadata[BG_METADATA_KEY];
saveMetadataDebounced(); saveMetadataDebounced();
} }
function setCustomBackground() { function setCustomBackground() {
const file = chat_metadata[METADATA_KEY]; const file = chat_metadata[BG_METADATA_KEY];
// bg already set // bg already set
if (document.getElementById("bg_custom").style.backgroundImage == file) { if (document.getElementById("bg_custom").style.backgroundImage == file) {
@ -100,6 +128,7 @@ function unsetCustomBackground() {
} }
function onSelectBackgroundClick() { function onSelectBackgroundClick() {
const isCustom = $(this).attr('custom') === 'true';
const relativeBgImage = getUrlParameter(this); const relativeBgImage = getUrlParameter(this);
// if clicked on upload button // if clicked on upload button
@ -107,15 +136,18 @@ function onSelectBackgroundClick() {
return; return;
} }
if (hasCustomBackground()) { // Automatically lock the background if it's custom or other background is locked
if (hasCustomBackground() || isCustom) {
saveBackgroundMetadata(relativeBgImage); saveBackgroundMetadata(relativeBgImage);
setCustomBackground(); setCustomBackground();
highlightLockedBackground();
} else {
highlightLockedBackground();
} }
highlightLockedBackground();
const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage; const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage;
// custom background is set. Do not override the layer below // Custom background is set. Do not override the layer below
if (customBg !== 'none') { if (customBg !== 'none') {
return; return;
} }
@ -123,7 +155,7 @@ function onSelectBackgroundClick() {
const bgFile = $(this).attr("bgfile"); const bgFile = $(this).attr("bgfile");
const backgroundUrl = getBackgroundPath(bgFile); const backgroundUrl = getBackgroundPath(bgFile);
// fetching to browser memory to reduce flicker // Fetching to browser memory to reduce flicker
fetch(backgroundUrl).then(() => { fetch(backgroundUrl).then(() => {
$("#bg1").css("background-image", relativeBgImage); $("#bg1").css("background-image", relativeBgImage);
setBackground(bgFile); setBackground(bgFile);
@ -132,32 +164,80 @@ function onSelectBackgroundClick() {
}); });
} }
async function onRenameBackgroundClick(e) { async function onCopyToSystemBackgroundClick(e) {
e.stopPropagation(); e.stopPropagation();
const old_bg = $(this).closest('.bg_example').attr('bgfile'); const bgNames = await getNewBackgroundName(this);
if (!old_bg) { if (!bgNames) {
return;
}
const bgFile = await fetch(bgNames.oldBg);
if (!bgFile.ok) {
toastr.warning('Failed to copy background');
return;
}
const blob = await bgFile.blob();
const file = new File([blob], bgNames.newBg);
const formData = new FormData();
formData.set('avatar', file);
uploadBackground(formData);
const list = chat_metadata[LIST_METADATA_KEY] || [];
const index = list.indexOf(bgNames.oldBg);
list.splice(index, 1);
saveMetadataDebounced();
getChatBackgroundsList();
}
/**
* Gets the new background name from the user.
* @param {Element} referenceElement
* @returns {Promise<{oldBg: string, newBg: string}>}
* */
async function getNewBackgroundName(referenceElement) {
const exampleBlock = $(referenceElement).closest('.bg_example');
const isCustom = exampleBlock.attr('custom') === 'true';
const oldBg = exampleBlock.attr('bgfile');
if (!oldBg) {
console.debug('no bgfile'); console.debug('no bgfile');
return; return;
} }
const fileExtension = old_bg.split('.').pop(); const fileExtension = oldBg.split('.').pop();
const old_bg_extensionless = old_bg.replace(`.${fileExtension}`, ''); const fileNameBase = isCustom ? oldBg.split('/').pop() : oldBg;
const new_bg_extensionless = await callPopup('<h3>Enter new background name:</h3>', 'input', old_bg_extensionless); const oldBgExtensionless = fileNameBase.replace(`.${fileExtension}`, '');
const newBgExtensionless = await callPopup('<h3>Enter new background name:</h3>', 'input', oldBgExtensionless);
if (!new_bg_extensionless) { if (!newBgExtensionless) {
console.debug('no new_bg_extensionless'); console.debug('no new_bg_extensionless');
return; return;
} }
const new_bg = `${new_bg_extensionless}.${fileExtension}`; const newBg = `${newBgExtensionless}.${fileExtension}`;
if (old_bg_extensionless === new_bg_extensionless) { if (oldBgExtensionless === newBgExtensionless) {
console.debug('new_bg === old_bg'); console.debug('new_bg === old_bg');
return; return;
} }
const data = { old_bg, new_bg }; return { oldBg, newBg };
}
async function onRenameBackgroundClick(e) {
e.stopPropagation();
const bgNames = await getNewBackgroundName(this);
if (!bgNames) {
return;
}
const data = { old_bg: bgNames.oldBg, new_bg: bgNames.newBg };
const response = await fetch('/renamebackground', { const response = await fetch('/renamebackground', {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders(),
@ -167,7 +247,7 @@ async function onRenameBackgroundClick(e) {
if (response.ok) { if (response.ok) {
await getBackgrounds(); await getBackgrounds();
highlightNewBackground(new_bg); highlightNewBackground(bgNames.newBg);
} else { } else {
toastr.warning('Failed to rename background'); toastr.warning('Failed to rename background');
} }
@ -177,28 +257,45 @@ async function onDeleteBackgroundClick(e) {
e.stopPropagation(); e.stopPropagation();
const bgToDelete = $(this).closest('.bg_example'); const bgToDelete = $(this).closest('.bg_example');
const url = bgToDelete.data('url'); const url = bgToDelete.data('url');
const isCustom = bgToDelete.attr('custom') === 'true';
const confirm = await callPopup("<h3>Delete the background?</h3>", 'confirm'); const confirm = await callPopup("<h3>Delete the background?</h3>", 'confirm');
const bg = bgToDelete.attr('bgfile');
if (confirm) { if (confirm) {
delBackground(bgToDelete.attr("bgfile")); // If it's not custom, it's a built-in background. Delete it from the server
if (!isCustom) {
delBackground(bg);
} else {
const list = chat_metadata[LIST_METADATA_KEY] || [];
const index = list.indexOf(bg);
list.splice(index, 1);
}
const siblingSelector = '.bg_example:not(#form_bg_download)'; const siblingSelector = '.bg_example:not(#form_bg_download)';
const nextBg = bgToDelete.next(siblingSelector); const nextBg = bgToDelete.next(siblingSelector);
const prevBg = bgToDelete.prev(siblingSelector); const prevBg = bgToDelete.prev(siblingSelector);
const anyBg = $(siblingSelector);
if (nextBg.length > 0) { if (nextBg.length > 0) {
nextBg.trigger('click'); nextBg.trigger('click');
} else { } else if (prevBg.length > 0) {
prevBg.trigger('click'); prevBg.trigger('click');
} else {
$(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click');
} }
bgToDelete.remove(); bgToDelete.remove();
if (url === chat_metadata[METADATA_KEY]) { if (url === chat_metadata[BG_METADATA_KEY]) {
removeBackgroundMetadata(); removeBackgroundMetadata();
unsetCustomBackground(); unsetCustomBackground();
highlightLockedBackground(); highlightLockedBackground();
} }
if (isCustom) {
getChatBackgroundsList();
saveMetadataDebounced();
}
} }
} }
@ -259,18 +356,20 @@ function getUrlParameter(block) {
* Instantiates a background template * Instantiates a background template
* @param {string} bg Path to background * @param {string} bg Path to background
* @param {boolean} isCustom Whether the background is custom * @param {boolean} isCustom Whether the background is custom
* @returns * @returns {JQuery<HTMLElement>} Background template
*/ */
function getBackgroundFromTemplate(bg, isCustom) { function getBackgroundFromTemplate(bg, isCustom) {
const thumbPath = getThumbnailUrl('bg', bg);
const template = $('#background_template .bg_example').clone(); const template = $('#background_template .bg_example').clone();
const url = isCustom ? `url("${bg}")` : `url("${getBackgroundPath(bg)}")`; const thumbPath = isCustom ? bg : getThumbnailUrl('bg', bg);
template.attr('title', bg); const url = isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;
const title = isCustom ? bg.split('/').pop() : bg;
const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
template.attr('title', title);
template.attr('bgfile', bg); template.attr('bgfile', bg);
template.attr('custom', String(isCustom)); template.attr('custom', String(isCustom));
template.data('url', url); template.data('url', url);
template.css('background-image', `url('${thumbPath}')`); template.css('background-image', `url('${thumbPath}')`);
template.find('.BGSampleTitle').text(bg.slice(0, bg.lastIndexOf('.'))); template.find('.BGSampleTitle').text(friendlyTitle);
return template; return template;
} }
@ -307,49 +406,43 @@ async function delBackground(bg) {
} }
function onBackgroundUploadSelected() { function onBackgroundUploadSelected() {
const input = this; const form = $("#form_bg_download").get(0);
if (input.files && input.files[0]) { if (!(form instanceof HTMLFormElement)) {
const reader = new FileReader(); console.error('form_bg_download is not a form');
return;
reader.onload = function (e) {
const form = $("#form_bg_download").get(0);
if (!(form instanceof HTMLFormElement)) {
console.error('form_bg_download is not a form');
return;
}
const formData = new FormData(form);
//console.log(formData);
jQuery.ajax({
type: "POST",
url: "/downloadbackground",
data: formData,
beforeSend: function () {
},
cache: false,
contentType: false,
processData: false,
success: async function (bg) {
form.reset();
setBackground(bg);
$("#bg1").css("background-image", `url("${getBackgroundPath(bg)}"`);
await getBackgrounds();
highlightNewBackground(bg);
},
error: function (jqXHR, exception) {
form.reset();
console.log(exception);
console.log(jqXHR);
},
});
};
reader.readAsDataURL(input.files[0]);
} }
const formData = new FormData(form);
uploadBackground(formData);
form.reset();
}
/**
* Uploads a background to the server
* @param {FormData} formData
*/
function uploadBackground(formData) {
jQuery.ajax({
type: "POST",
url: "/downloadbackground",
data: formData,
beforeSend: function () {
},
cache: false,
contentType: false,
processData: false,
success: async function (bg) {
setBackground(bg);
$("#bg1").css("background-image", `url("${getBackgroundPath(bg)}"`);
await getBackgrounds();
highlightNewBackground(bg);
},
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
} }
/** /**
@ -378,11 +471,12 @@ function onBackgroundFilterInput() {
export function initBackgrounds() { export function initBackgrounds() {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged); eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
$(document).on("click", ".bg_example", onSelectBackgroundClick); $(document).on("click", '.bg_example', onSelectBackgroundClick);
$(document).on('click', '.bg_example_lock', onLockBackgroundClick); $(document).on('click', '.bg_example_lock', onLockBackgroundClick);
$(document).on('click', '.bg_example_unlock', onUnlockBackgroundClick); $(document).on('click', '.bg_example_unlock', onUnlockBackgroundClick);
$(document).on('click', '.bg_example_edit', onRenameBackgroundClick); $(document).on('click', '.bg_example_edit', onRenameBackgroundClick);
$(document).on("click", ".bg_example_cross", onDeleteBackgroundClick); $(document).on("click", '.bg_example_cross', onDeleteBackgroundClick);
$(document).on("click", '.bg_example_copy', onCopyToSystemBackgroundClick);
$('#auto_background').on("click", autoBackgroundCommand); $('#auto_background').on("click", autoBackgroundCommand);
$("#add_bg_button").on('change', onBackgroundUploadSelected); $("#add_bg_button").on('change', onBackgroundUploadSelected);
$("#bg-filter").on("input", onBackgroundFilterInput); $("#bg-filter").on("input", onBackgroundFilterInput);

View File

@ -1,8 +1,7 @@
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams } from "../../../script.js"; import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams } from "../../../script.js";
import { getContext, extension_settings } from "../../extensions.js"; import { getContext, extension_settings } from "../../extensions.js";
import { initScrollHeight, resetScrollHeight } from "../../utils.js"; import { initScrollHeight, resetScrollHeight } from "../../utils.js";
import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "../../slash-commands.js"; import { registerSlashCommand } from "../../slash-commands.js";
export { MODULE_NAME }; export { MODULE_NAME };
@ -15,8 +14,9 @@ const defaultSettings = {
quickReplyEnabled: false, quickReplyEnabled: false,
numberOfSlots: 5, numberOfSlots: 5,
quickReplySlots: [], quickReplySlots: [],
placeBeforePromptEnabled: false, placeBeforeInputEnabled: false,
quickActionEnabled: false, quickActionEnabled: false,
AutoInputInject: true,
} }
//method from worldinfo //method from worldinfo
@ -35,8 +35,12 @@ async function updateQuickReplyPresetList() {
if (presets !== undefined) { if (presets !== undefined) {
presets.forEach((item, i) => { presets.forEach((item) => {
$("#quickReplyPresets").append(`<option value='${item.name}'${selected_preset.includes(item.name) ? ' selected' : ''}>${item.name}</option>`); const option = document.createElement('option');
option.value = item.name;
option.innerText = item.name;
option.selected = selected_preset.includes(item.name);
$("#quickReplyPresets").append(option);
}); });
} }
} }
@ -50,6 +54,10 @@ async function loadSettings(type) {
Object.assign(extension_settings.quickReply, defaultSettings); Object.assign(extension_settings.quickReply, defaultSettings);
} }
if (extension_settings.quickReply.AutoInputInject === undefined) {
extension_settings.quickReply.AutoInputInject = true;
}
// If the user has an old version of the extension, update it // If the user has an old version of the extension, update it
if (!Array.isArray(extension_settings.quickReply.quickReplySlots)) { if (!Array.isArray(extension_settings.quickReply.quickReplySlots)) {
extension_settings.quickReply.quickReplySlots = []; extension_settings.quickReply.quickReplySlots = [];
@ -77,20 +85,21 @@ async function loadSettings(type) {
$('#quickReplyEnabled').prop('checked', extension_settings.quickReply.quickReplyEnabled); $('#quickReplyEnabled').prop('checked', extension_settings.quickReply.quickReplyEnabled);
$('#quickReplyNumberOfSlots').val(extension_settings.quickReply.numberOfSlots); $('#quickReplyNumberOfSlots').val(extension_settings.quickReply.numberOfSlots);
$('#placeBeforePromptEnabled').prop('checked', extension_settings.quickReply.placeBeforePromptEnabled); $('#placeBeforeInputEnabled').prop('checked', extension_settings.quickReply.placeBeforeInputEnabled);
$('#quickActionEnabled').prop('checked', extension_settings.quickReply.quickActionEnabled); $('#quickActionEnabled').prop('checked', extension_settings.quickReply.quickActionEnabled);
$('#AutoInputInject').prop('checked', extension_settings.quickReply.AutoInputInject);
} }
function onQuickReplyInput(id) { function onQuickReplyInput(id) {
extension_settings.quickReply.quickReplySlots[id - 1].mes = $(`#quickReply${id}Mes`).val(); extension_settings.quickReply.quickReplySlots[id - 1].mes = $(`#quickReply${id}Mes`).val();
$(`#quickReply${id}`).attr('title', ($(`#quickReply${id}Mes`).val())); $(`#quickReply${id}`).attr('title', String($(`#quickReply${id}Mes`).val()));
resetScrollHeight($(`#quickReply${id}Mes`)); resetScrollHeight($(`#quickReply${id}Mes`));
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onQuickReplyLabelInput(id) { function onQuickReplyLabelInput(id) {
extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val(); extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val();
$(`#quickReply${id}`).text($(`#quickReply${id}Label`).val()); $(`#quickReply${id}`).text(String($(`#quickReply${id}Label`).val()));
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -109,8 +118,13 @@ async function onQuickActionEnabledInput() {
saveSettingsDebounced(); saveSettingsDebounced();
} }
async function onPlaceBeforePromptEnabledInput() { async function onPlaceBeforeInputEnabledInput() {
extension_settings.quickReply.placeBeforePromptEnabled = !!$(this).prop('checked'); extension_settings.quickReply.placeBeforeInputEnabled = !!$(this).prop('checked');
saveSettingsDebounced();
}
async function onAutoInputInject() {
extension_settings.quickReply.AutoInputInject = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -125,16 +139,15 @@ async function sendQuickReply(index) {
let newText; let newText;
if (existingText) { if (existingText && extension_settings.quickReply.AutoInputInject) {
// If existing text, add space after prompt if (extension_settings.quickReply.placeBeforeInputEnabled) {
if (extension_settings.quickReply.placeBeforePromptEnabled) {
newText = `${prompt} ${existingText} `; newText = `${prompt} ${existingText} `;
} else { } else {
newText = `${existingText} ${prompt} `; newText = `${existingText} ${prompt} `;
} }
} else { } else {
// If no existing text, add prompt only (with a trailing space) // If no existing text and placeBeforeInputEnabled false, add prompt only (with a trailing space)
newText = prompt + ' '; newText = `${prompt} `;
} }
newText = substituteParams(newText); newText = substituteParams(newText);
@ -142,7 +155,7 @@ async function sendQuickReply(index) {
$("#send_textarea").val(newText); $("#send_textarea").val(newText);
// Set the focus back to the textarea // Set the focus back to the textarea
$("#send_textarea").focus(); $("#send_textarea").trigger('focus');
// Only trigger send button if quickActionEnabled is not checked or // Only trigger send button if quickActionEnabled is not checked or
// the prompt starts with '/' // the prompt starts with '/'
@ -221,7 +234,7 @@ async function saveQuickReplyPreset() {
} }
else { else {
presets[quickReplyPresetIndex] = quickReplyPreset; presets[quickReplyPresetIndex] = quickReplyPreset;
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true); $(`#quickReplyPresets option[value="${name}"]`).prop('selected', true);
} }
saveSettingsDebounced(); saveSettingsDebounced();
} else { } else {
@ -274,8 +287,8 @@ function generateQuickReplyElements() {
for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) { for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) {
quickReplyHtml += ` quickReplyHtml += `
<div class="flex-container alignitemsflexstart"> <div class="flex-container alignitemsflexstart">
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Add a button label)"> <input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Button label)">
<textarea id="quickReply${i}Mes" placeholder="(custom message here)" class="text_pole widthUnset flex1" rows="2"></textarea> <textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1" rows="2"></textarea>
</div> </div>
`; `;
} }
@ -309,7 +322,7 @@ async function applyQuickReplyPreset(name) {
addQuickReplyBar(); addQuickReplyBar();
moduleWorker(); moduleWorker();
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true); $(`#quickReplyPresets option[value="${name}"]`).prop('selected', true);
console.debug('QR Preset applied: ' + name); console.debug('QR Preset applied: ' + name);
} }
@ -334,7 +347,6 @@ async function doQR(_, text) {
} }
jQuery(async () => { jQuery(async () => {
moduleWorker(); moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL); setInterval(moduleWorker, UPDATE_INTERVAL);
const settingsHtml = ` const settingsHtml = `
@ -348,20 +360,28 @@ jQuery(async () => {
<div> <div>
<label class="checkbox_label"> <label class="checkbox_label">
<input id="quickReplyEnabled" type="checkbox" /> <input id="quickReplyEnabled" type="checkbox" />
Enable Quick Replies Enable Quick Replies
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input id="quickActionEnabled" type="checkbox" /> <input id="quickActionEnabled" type="checkbox" />
Disable Send / Insert In User Input Disable Send / Insert In User Input
</label> </label>
<label class="checkbox_label marginBot10"> <label class="checkbox_label marginBot10">
<input id="placeBeforePromptEnabled" type="checkbox" /> <input id="placeBeforeInputEnabled" type="checkbox" />
Place Quick-reply before the Prompt Place Quick-reply before the Input
</label> </label>
<label class="checkbox_label marginBot10">
<input id="AutoInputInject" type="checkbox" />
Inject user input automatically<br>(If disabled, use {{input}} macro for manual injection)
</label>
<label for="quickReplyPresets">Quick Reply presets:</label>
<div class="flex-container flexnowrap wide100p"> <div class="flex-container flexnowrap wide100p">
<select id="quickReplyPresets" name="quickreply-preset"> <select id="quickReplyPresets" name="quickreply-preset" class="flex1 text_pole">
</select> </select>
<i id="quickReplyPresetSaveButton" class="fa-solid fa-save"></i> <div id="quickReplyPresetSaveButton" class="menu_button menu_button_icon">
<div class="fa-solid fa-save"></div>
<span>Save</span>
</div>
</div> </div>
<label for="quickReplyNumberOfSlots">Number of slots:</label> <label for="quickReplyNumberOfSlots">Number of slots:</label>
</div> </div>
@ -382,7 +402,8 @@ jQuery(async () => {
// Add event handler for quickActionEnabled // Add event handler for quickActionEnabled
$('#quickActionEnabled').on('input', onQuickActionEnabledInput); $('#quickActionEnabled').on('input', onQuickActionEnabledInput);
$('#placeBeforePromptEnabled').on('input', onPlaceBeforePromptEnabledInput); $('#placeBeforeInputEnabled').on('input', onPlaceBeforeInputEnabledInput);
$('#AutoInputInject').on('input', onAutoInputInject);
$('#quickReplyEnabled').on('input', onQuickReplyEnabledInput); $('#quickReplyEnabled').on('input', onQuickReplyEnabledInput);
$('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput); $('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput);
$("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset); $("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset);
@ -392,16 +413,13 @@ jQuery(async () => {
extension_settings.quickReplyPreset = quickReplyPresetSelected; extension_settings.quickReplyPreset = quickReplyPresetSelected;
applyQuickReplyPreset(quickReplyPresetSelected); applyQuickReplyPreset(quickReplyPresetSelected);
saveSettingsDebounced(); saveSettingsDebounced();
}); });
await loadSettings('init'); await loadSettings('init');
addQuickReplyBar(); addQuickReplyBar();
}); });
$(document).ready(() => { jQuery(() => {
registerSlashCommand('qr', doQR, [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true); registerSlashCommand('qr', doQR, [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true);
registerSlashCommand('qrset', doQRPresetSwitch, [], '<span class="monospace">(name)</span> swaps to the specified Quick Reply Preset', true, true); registerSlashCommand('qrset', doQRPresetSwitch, [], '<span class="monospace">(name)</span> swaps to the specified Quick Reply Preset', true, true);
}) })

View File

@ -1116,10 +1116,9 @@ async function generatePicture(_, trigger, message, callback) {
extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64; extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
} }
const callbackOriginal = callback; const callbackOriginal = callback;
callback = async function (prompt, base64Image) { callback = async function (prompt, imagePath) {
const imagePath = base64Image; const imgUrl = `url("${encodeURI(imagePath)}")`;
const imgUrl = `url("${encodeURI(base64Image)}")`; eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
eventSource.emit(event_types.FORCE_SET_BACKGROUND, imgUrl);
if (typeof callbackOriginal === 'function') { if (typeof callbackOriginal === 'function') {
callbackOriginal(prompt, imagePath); callbackOriginal(prompt, imagePath);

View File

@ -1,6 +1,7 @@
import { callPopup, main_api } from "../../../script.js"; import { callPopup, main_api } from "../../../script.js";
import { getContext } from "../../extensions.js"; import { getContext } from "../../extensions.js";
import { getTokenizerModel } from "../../tokenizers.js"; import { registerSlashCommand } from "../../slash-commands.js";
import { getTokenCount, getTokenizerModel } from "../../tokenizers.js";
async function doTokenCounter() { async function doTokenCounter() {
const selectedTokenizer = main_api == 'openai' const selectedTokenizer = main_api == 'openai'
@ -29,6 +30,20 @@ async function doTokenCounter() {
callPopup(dialog, 'text'); callPopup(dialog, 'text');
} }
function doCount() {
// get all of the messages in the chat
const context = getContext();
const messages = context.chat.filter(x => x.mes && !x.is_system).map(x => x.mes);
//concat all the messages into a single string
const allMessages = messages.join(' ');
console.debug('All messages:', allMessages);
//toastr success with the token count of the chat
toastr.success(`Token count: ${getTokenCount(allMessages)}`);
}
jQuery(() => { jQuery(() => {
const buttonHtml = ` const buttonHtml = `
<div id="token_counter" class="list-group-item flex-container flexGap5"> <div id="token_counter" class="list-group-item flex-container flexGap5">
@ -37,4 +52,5 @@ jQuery(() => {
</div>`; </div>`;
$('#extensionsMenu').prepend(buttonHtml); $('#extensionsMenu').prepend(buttonHtml);
$('#token_counter').on('click', doTokenCounter); $('#token_counter').on('click', doTokenCounter);
registerSlashCommand('count', doCount, [], ' counts the number of tokens in the current chat', true, false);
}); });

View File

@ -399,6 +399,7 @@ function switchMessageActions() {
power_user.expand_message_actions = value === null ? false : value == "true"; power_user.expand_message_actions = value === null ? false : value == "true";
$("body").toggleClass("expandMessageActions", power_user.expand_message_actions); $("body").toggleClass("expandMessageActions", power_user.expand_message_actions);
$("#expandMessageActions").prop("checked", power_user.expand_message_actions); $("#expandMessageActions").prop("checked", power_user.expand_message_actions);
$('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
} }
function switchUiMode() { function switchUiMode() {

View File

@ -3,6 +3,7 @@ System-wide Replacement Macros:
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> - your current Persona username</li> <li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> - your current Persona username</li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> - the Character's name</li> <li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> - the Character's name</li>
<li><tt>&lcub;&lcub;input&rcub;&rcub;</tt> - the user input</li> <li><tt>&lcub;&lcub;input&rcub;&rcub;</tt> - the user input</li>
<li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> - you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li>
<li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> - the current time</li> <li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> - the current time</li>
<li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> - the current date</li> <li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> - the current date</li>
<li><tt>&lcub;&lcub;weekday&rcub;&rcub;</tt> - the current weekday</li> <li><tt>&lcub;&lcub;weekday&rcub;&rcub;</tt> - the current weekday</li>

View File

@ -870,7 +870,7 @@ export async function saveBase64AsFile(base64Data, characterName, filename = "",
// If the response is successful, get the saved image path from the server's response // If the response is successful, get the saved image path from the server's response
if (response.ok) { if (response.ok) {
const responseData = await response.json(); const responseData = await response.json();
return responseData.path; return responseData.path.replace(/\\/g, '/'); // Replace backslashes with forward slashes
} else { } else {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || 'Failed to upload the image to the server'); throw new Error(errorData.error || 'Failed to upload the image to the server');

View File

@ -1506,7 +1506,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
z-index: 3001; z-index: 3001;
} }
#bg_menu_content { .bg_list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
width: calc(var(--sheldWidth) - 10px); width: calc(var(--sheldWidth) - 10px);
@ -1543,6 +1543,14 @@ input[type=search]:focus::-webkit-search-cancel-button {
display: none; display: none;
} }
.bg_example:hover[custom="true"] .bg_example_edit {
display: none;
}
.bg_example:hover[custom="false"] .bg_example_copy {
display: none;
}
.BGSampleTitle { .BGSampleTitle {
display: flex; display: flex;
width: 100%; width: 100%;
@ -1591,6 +1599,10 @@ input[type=search]:focus::-webkit-search-cancel-button {
left: 10px; left: 10px;
} }
.bg_example_copy {
left: 10px;
}
.bg_example_lock, .bg_example_lock,
.bg_example_unlock { .bg_example_unlock {
left: 50%; left: 50%;

View File

@ -726,7 +726,7 @@ app.post("/getchat", jsonParser, function (request, response) {
const lines = data.split('\n'); const lines = data.split('\n');
// Iterate through the array of strings and parse each line as JSON // Iterate through the array of strings and parse each line as JSON
const jsonData = lines.map(tryParse).filter(x => x); const jsonData = lines.map((l) => { try { return JSON.parse(l); } catch (_) { } }).filter(x => x);
return response.send(jsonData); return response.send(jsonData);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -1841,37 +1841,27 @@ function getImages(path) {
.sort(Intl.Collator().compare); .sort(Intl.Collator().compare);
} }
app.post("/getallchatsofcharacter", jsonParser, function (request, response) { app.post("/getallchatsofcharacter", jsonParser, async function (request, response) {
if (!request.body) return response.sendStatus(400); if (!request.body) return response.sendStatus(400);
var char_dir = (request.body.avatar_url).replace('.png', '') const characterDirectory = (request.body.avatar_url).replace('.png', '');
fs.readdir(chatsPath + char_dir, (err, files) => {
if (err) { try {
console.log('found error in history loading'); const chatsDirectory = path.join(chatsPath, characterDirectory);
console.error(err); const files = fs.readdirSync(chatsDirectory);
const jsonFiles = files.filter(file => path.extname(file) === '.jsonl');
if (jsonFiles.length === 0) {
response.send({ error: true }); response.send({ error: true });
return; return;
} }
// filter for JSON files const jsonFilesPromise = jsonFiles.map((file) => {
const jsonFiles = files.filter(file => path.extname(file) === '.jsonl'); return new Promise(async (res) => {
const pathToFile = path.join(chatsPath, characterDirectory, file);
// sort the files by name const fileStream = fs.createReadStream(pathToFile);
//jsonFiles.sort().reverse(); const stats = fs.statSync(pathToFile);
// print the sorted file names const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
var chatData = {};
let ii = jsonFiles.length; //this is the number of files belonging to the character
if (ii !== 0) {
//console.log('found '+ii+' chat logs to load');
for (let i = jsonFiles.length - 1; i >= 0; i--) {
const file = jsonFiles[i];
const fileStream = fs.createReadStream(chatsPath + char_dir + '/' + file);
const fullPathAndFile = chatsPath + char_dir + '/' + file
const stats = fs.statSync(fullPathAndFile);
const fileSizeInKB = (stats.size / 1024).toFixed(2) + "kb";
//console.log(fileSizeInKB);
const rl = readline.createInterface({ const rl = readline.createInterface({
input: fileStream, input: fileStream,
@ -1885,34 +1875,37 @@ app.post("/getallchatsofcharacter", jsonParser, function (request, response) {
lastLine = line; lastLine = line;
}); });
rl.on('close', () => { rl.on('close', () => {
ii--; rl.close();
if (lastLine) {
let jsonData = tryParse(lastLine); if (lastLine) {
if (jsonData && (jsonData.name !== undefined || jsonData.character_name !== undefined)) { const jsonData = tryParse(lastLine);
chatData[i] = {}; if (jsonData && (jsonData.name || jsonData.character_name)) {
chatData[i]['file_name'] = file; const chatData = {};
chatData[i]['file_size'] = fileSizeInKB;
chatData[i]['chat_items'] = itemCounter - 1; chatData['file_name'] = file;
chatData[i]['mes'] = jsonData['mes'] || '[The chat is empty]'; chatData['file_size'] = fileSizeInKB;
chatData[i]['last_mes'] = jsonData['send_date'] || Date.now(); chatData['chat_items'] = itemCounter - 1;
chatData['mes'] = jsonData['mes'] || '[The chat is empty]';
chatData['last_mes'] = jsonData['send_date'] || Date.now();
res(chatData);
} else { } else {
console.log('Found an invalid or corrupted chat file: ' + fullPathAndFile); console.log('Found an invalid or corrupted chat file:', pathToFile);
res({});
} }
} }
if (ii === 0) {
//console.log('ii count went to zero, responding with chatData');
response.send(chatData);
}
//console.log('successfully closing getallchatsofcharacter');
rl.close();
}); });
}; });
} else { });
//console.log('Found No Chats. Exiting Load Routine.');
response.send({ error: true }); const chatData = await Promise.all(jsonFilesPromise);
}; const validFiles = chatData.filter(i => i.file_name);
})
return response.send(validFiles);
} catch (error) {
console.log(error);
return response.send({ error: true });
}
}); });
function getPngName(file) { function getPngName(file) {