diff --git a/public/script.js b/public/script.js
index 842746dcf..950e0392b 100644
--- a/public/script.js
+++ b/public/script.js
@@ -49,6 +49,7 @@ import {
editGroup,
deleteGroupChat,
renameGroupChat,
+ importGroupChat,
} from "./scripts/group-chats.js";
import {
@@ -106,7 +107,7 @@ import {
setPoeOnlineStatus,
} from "./scripts/poe.js";
-import { debounce, delay, restoreCaretPosition, saveCaretPosition, end_trim_to_sentence } from "./scripts/utils.js";
+import { debounce, delay, restoreCaretPosition, saveCaretPosition, end_trim_to_sentence, countOccurrences, isOdd } from "./scripts/utils.js";
import { extension_settings, loadExtensionSettings } from "./scripts/extensions.js";
import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js";
import {
@@ -226,7 +227,6 @@ let safetychat = [
];
let chat_create_date = 0;
-let prev_selected_char = null;
const default_ch_mes = "Hello";
let count_view_mes = 0;
let mesStr = "";
@@ -293,7 +293,7 @@ const system_messages = {
mes: [
`Hi there! The following chat formatting commands are supported:
- - {{text}} - sets a one-time behavioral bias for the AI. Resets when you send the next message.
+
- {{text}} - sets a one-time behavioral bias for the AI. Resets when you send the next message.
Hotkeys/Keybinds:
@@ -1026,6 +1026,11 @@ function messageFormatting(mes, ch_name, isSystem, isUser) {
});
}
+ // Hides bias from empty messages send with slash commands
+ if (isSystem) {
+ mes = mes.replace(/{{(\*?.*\*?)}}/g, "");
+ }
+
if (!power_user.allow_name2_display && ch_name && !isUser && !isSystem) {
mes = mes.replaceAll(`${ch_name}:`, "");
}
@@ -1159,7 +1164,7 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true
newMessage.data("isSystem", isSystem);
if (isSystem) {
- newMessage.find(".mes_edit").hide();
+ // newMessage.find(".mes_edit").hide();
newMessage.find(".mes_prompt").hide(); //don't need prompt button for sys
}
@@ -1319,7 +1324,7 @@ function processCommands(message, type) {
return result.interrupt;
}
-function sendSystemMessage(type, text) {
+function sendSystemMessage(type, text, extra = {}) {
const systemMessage = system_messages[type];
if (!systemMessage) {
@@ -1340,6 +1345,7 @@ function sendSystemMessage(type, text) {
newMessage.extra = {};
}
+ newMessage.extra = Object.assign(newMessage.extra, extra);
newMessage.extra.type = type;
chat.push(newMessage);
@@ -1347,7 +1353,7 @@ function sendSystemMessage(type, text) {
is_send_press = false;
}
-function extractMessageBias(message) {
+export function extractMessageBias(message) {
if (!message) {
return null;
}
@@ -1509,6 +1515,12 @@ class StreamingProcessor {
let isName = result.this_mes_is_name;
processedText = result.getMessage;
+ // Predict unbalanced asterisks during streaming
+ if (!isFinal && isOdd(countOccurrences(processedText, '*'))) {
+ // Add asterisk at the end to balance it
+ processedText = processedText.trimEnd() + '*';
+ }
+
if (isImpersonate) {
$('#send_textarea').val(processedText).trigger('input');
}
@@ -1743,7 +1755,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
//for normal messages sent from user..
if (textareaText != "" && !automatic_trigger && type !== 'quiet') {
- sendMessageAsUser(textareaText, messageBias);
+ // If user message contains no text other than bias - send as a system message
+ if (messageBias && replaceBiasMarkup(textareaText).trim().length === 0) {
+ sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });
+ }
+ else {
+ sendMessageAsUser(textareaText, messageBias);
+ }
}
////////////////////////////////////
const scenarioText = chat_metadata['scenario'] || characters[this_chid].scenario;
@@ -2359,8 +2377,8 @@ function getBiasStrings(textareaText) {
// gets bias of the latest message where it was applied
for (let mes of chat.slice().reverse()) {
- if (mes && mes.is_user) {
- if (mes.extra && mes.extra.bias && mes.extra.bias.trim().length > 0) {
+ if (mes && mes.extra && (mes.is_user || mes.is_system || mes.extra.type === system_message_types.NARRATOR)) {
+ if (mes.extra.bias && mes.extra.bias.trim().length > 0) {
promptBias = mes.extra.bias;
}
break;
@@ -2383,12 +2401,15 @@ function formatMessageHistoryItem(chatItem, isInstruct) {
textResult = formatInstructModeChat(itemName, chatItem.mes, chatItem.is_user, isNarratorType, chatItem.force_avatar);
}
- // replace bias markup
- textResult = (textResult ?? '').replace(/{{(\*?.*\*?)}}/g, '');
+ textResult = replaceBiasMarkup(textResult);
return textResult;
}
+export function replaceBiasMarkup(str) {
+ return (str ?? '').replace(/{{(\*?.*\*?)}}/g, '');
+}
+
function sendMessageAsUser(textareaText, messageBias) {
chat[chat.length] = {};
chat[chat.length - 1]['name'] = name1;
@@ -2431,9 +2452,6 @@ function getMaxContextSize() {
}
function parseTokenCounts(counts, thisPromptBits) {
- const breakdown_bar = $('#token_breakdown div:first-child');
- breakdown_bar.empty();
-
const total = Object.values(counts).filter(x => !Number.isNaN(x)).reduce((acc, val) => acc + val, 0);
thisPromptBits.push({
@@ -2447,22 +2465,6 @@ function parseTokenCounts(counts, thisPromptBits) {
oaiConversationTokens: Object.entries(counts)[7][1],
oaiTotalTokens: total,
});
-
- Object.entries(counts).forEach(([type, value]) => {
- if (value === 0) {
- return;
- }
- const percent_value = (value / total) * 100;
- const color = uniqolor(type, { saturation: 50, lightness: 75, }).color;
- const bar = document.createElement('div');
- bar.style.width = `${percent_value}%`;
- bar.classList.add('token_breakdown_segment');
- bar.style.backgroundColor = color + 'AA';
- bar.style.borderColor = color + 'FF';
- bar.innerText = value;
- bar.title = `${type}: ${percent_value.toFixed(2)}%`;
- breakdown_bar.append(bar);
- });
}
function adjustChatsSeparator(mesSendString) {
@@ -3409,6 +3411,7 @@ async function read_avatar_load(input) {
await delay(durationSaveEdit);
await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
method: 'GET',
+ cache: 'no-cache',
headers: {
'pragma': 'no-cache',
'cache-control': 'no-cache',
@@ -3580,10 +3583,8 @@ function changeMainAPI() {
// Hide common settings for OpenAI
if (selectedVal == "openai") {
$("#common-gen-settings-block").css("display", "none");
- //$("#token_breakdown").css("display", "flex");
} else {
$("#common-gen-settings-block").css("display", "block");
- //$("#token_breakdown").css("display", "none");
}
// Hide amount gen for poe
if (selectedVal == "poe") {
@@ -3901,22 +3902,30 @@ function messageEditAuto(div) {
let mesBlock = div.closest(".mes_block");
var text = mesBlock.find(".edit_textarea").val().trim();
const bias = extractMessageBias(text);
- chat[this_edit_mes_id]["mes"] = text;
- if (chat[this_edit_mes_id]["swipe_id"] !== undefined) {
- chat[this_edit_mes_id]["swipes"][chat[this_edit_mes_id]["swipe_id"]] = text;
+ const mes = chat[this_edit_mes_id];
+ mes["mes"] = text;
+ if (mes["swipe_id"] !== undefined) {
+ mes["swipes"][mes["swipe_id"]] = text;
}
// editing old messages
- if (!chat[this_edit_mes_id]["extra"]) {
- chat[this_edit_mes_id]["extra"] = {};
+ if (!mes["extra"]) {
+ mes["extra"] = {};
}
- chat[this_edit_mes_id]["extra"]["bias"] = bias ?? null;
+
+ if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) {
+ mes.extra.bias = bias ?? null;
+ }
+ else {
+ mes.extra.bias = null;
+ }
+
mesBlock.find(".mes_text").val('');
mesBlock.find(".mes_text").val(messageFormatting(
text,
this_edit_mes_chname,
- chat[this_edit_mes_id].is_system,
- chat[this_edit_mes_id].is_user,
+ mes.is_system,
+ mes.is_user,
));
saveChatDebounced();
}
@@ -3925,17 +3934,23 @@ function messageEditDone(div) {
let mesBlock = div.closest(".mes_block");
var text = mesBlock.find(".edit_textarea").val().trim();
const bias = extractMessageBias(text);
- chat[this_edit_mes_id]["mes"] = text;
- if (chat[this_edit_mes_id]["swipe_id"] !== undefined) {
- chat[this_edit_mes_id]["swipes"][chat[this_edit_mes_id]["swipe_id"]] = text;
+ const mes = chat[this_edit_mes_id];
+ mes["mes"] = text;
+ if (mes["swipe_id"] !== undefined) {
+ mes["swipes"][mes["swipe_id"]] = text;
}
// editing old messages
- if (!chat[this_edit_mes_id]["extra"]) {
- chat[this_edit_mes_id]["extra"] = {};
+ if (!mes.extra) {
+ mes.extra = {};
}
- chat[this_edit_mes_id]["extra"]["bias"] = bias ?? null;
+ if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) {
+ mes.extra.bias = bias ?? null;
+ }
+ else {
+ mes.extra.bias = null;
+ }
mesBlock.find(".mes_text").empty();
mesBlock.find(".mes_edit_buttons").css("display", "none");
@@ -3944,13 +3959,13 @@ function messageEditDone(div) {
messageFormatting(
text,
this_edit_mes_chname,
- chat[this_edit_mes_id].is_system,
- chat[this_edit_mes_id].is_user,
+ mes.is_system,
+ mes.is_user,
)
);
mesBlock.find(".mes_bias").empty();
mesBlock.find(".mes_bias").append(messageFormatting(bias));
- appendImageToMessage(chat[this_edit_mes_id], div.closest(".mes"));
+ appendImageToMessage(mes, div.closest(".mes"));
addCopyToCodeBlocks(div.closest(".mes"));
this_edit_mes_id = undefined;
saveChatConditional();
@@ -3973,7 +3988,7 @@ async function getPastCharacterChats() {
return data;
}
-async function displayPastChats() {
+export async function displayPastChats() {
$("#select_chat_div").empty();
const group = selected_group ? groups.find(x => x.id === selected_group) : null;
@@ -4010,7 +4025,7 @@ async function displayPastChats() {
$("#select_chat_div").append(template);
- if (currentChat === fileName.replace(".jsonl", "")) {
+ if (currentChat === fileName.toString().replace(".jsonl", "")) {
$("#select_chat_div").find(".select_chat_block:last").attr("highlight", true);
}
}
@@ -4055,7 +4070,6 @@ async function getStatusNovel() {
}
}
-
function selectRightMenuWithAnimation(selectedMenuId) {
const displayModes = {
'rm_info_block': 'flex',
@@ -4076,13 +4090,7 @@ function selectRightMenuWithAnimation(selectedMenuId) {
easing: animation_easing,
complete: function () { },
});
-
-
-
- // $(menu).find('#groupCurrentMemberListToggle').click();
-
}
-
})
}
@@ -4096,8 +4104,7 @@ function setRightTabSelectedClass(selectedButtonId) {
});
}
-function select_rm_info(type, charId) {
-
+function select_rm_info(type, charId, previousCharId = null) {
if (!type) {
toastr.error(`Invalid process (no 'type')`);
return;
@@ -4124,16 +4131,12 @@ function select_rm_info(type, charId) {
setTimeout(function () {
$(`#rm_characters_block [title="${charId + '.png'}"]`).parent().removeClass('flash animated');
}, 5000);
-
}
-
setRightTabSelectedClass();
- prev_selected_char = charId;
-
- if (prev_selected_char) {
- const newId = characters.findIndex((x) => x.name == prev_selected_char);
+ if (previousCharId) {
+ const newId = characters.findIndex((x) => x.avatar == previousCharId);
if (newId >= 0) {
this_chid = newId;
}
@@ -4240,21 +4243,11 @@ function select_rm_create() {
}
function select_rm_characters() {
- restoreSelectedCharacter();
-
menu_type = "characters";
selectRightMenuWithAnimation('rm_characters_block');
setRightTabSelectedClass('rm_button_characters');
}
-function restoreSelectedCharacter() {
- if (prev_selected_char) {
- let newChId = characters.findIndex((x) => x.name == prev_selected_char);
- $(`.character_select[chid="${newChId}"]`).trigger("click");
- prev_selected_char = null;
- }
-}
-
function setExtensionPrompt(key, value, position, depth) {
extension_prompts[key] = { value, position, depth };
}
@@ -4457,6 +4450,27 @@ export async function saveChatConditional() {
}
}
+async function importCharacterChat(formData) {
+ await jQuery.ajax({
+ type: "POST",
+ url: "/importchat",
+ data: formData,
+ beforeSend: function () {
+ },
+ cache: false,
+ contentType: false,
+ processData: false,
+ success: async function (data) {
+ if (data.res) {
+ await displayPastChats();
+ }
+ },
+ error: function () {
+ $("#create_button").removeAttr("disabled");
+ },
+ });
+}
+
function updateViewMessageIds() {
$('#chat').find(".mes").each(function (index, element) {
$(element).attr("mesid", index);
@@ -4516,7 +4530,7 @@ function isHordeGenerationNotAllowed() {
}
export function cancelTtsPlay() {
- if (speechSynthesis) {
+ if ('speechSynthesis' in window) {
speechSynthesis.cancel();
}
}
@@ -5364,7 +5378,7 @@ $(document).ready(function () {
$("#create_button").attr("value", "✅");
let oldSelectedChar = null;
if (this_chid != undefined && this_chid != "invalid-safety-id") {
- oldSelectedChar = characters[this_chid].name;
+ oldSelectedChar = characters[this_chid].avatar;
}
console.log(`new avatar id: ${html}`);
@@ -5374,7 +5388,7 @@ $(document).ready(function () {
$("#rm_info_block").transition({ opacity: 0, duration: 0 });
var $prev_img = $("#avatar_div_div").clone();
$("#rm_info_avatar").append($prev_img);
- select_rm_info(`char_create`, save_name);
+ select_rm_info(`char_create`, save_name, oldSelectedChar);
$("#rm_info_block").transition({ opacity: 1.0, duration: 2000 });
crop_data = undefined;
@@ -5963,11 +5977,12 @@ $(document).ready(function () {
//***Message Editor***
$(document).on("click", ".mes_edit", function () {
if (this_chid !== undefined || selected_group) {
- const message = $(this).closest(".mes");
+ // Previously system messages we're allowed to be edited
+ /*const message = $(this).closest(".mes");
if (message.data("isSystem")) {
return;
- }
+ }*/
let chatScrollPosition = $("#chat").scrollTop();
if (this_edit_mes_id !== undefined) {
@@ -6197,8 +6212,6 @@ $(document).ready(function () {
return;
}
- let names = [];
-
for (const file of e.target.files) {
var ext = file.name.match(/\.(\w+)$/);
if (
@@ -6235,13 +6248,11 @@ $(document).ready(function () {
let oldSelectedChar = null;
if (this_chid != undefined && this_chid != "invalid-safety-id") {
- oldSelectedChar = characters[this_chid].name;
+ oldSelectedChar = characters[this_chid].avatar;
}
- names.push(data.file_name);
- let nameString = DOMPurify.sanitize(names.join(', '));
await getCharacters();
- select_rm_info(`char_import`, data.file_name);
+ select_rm_info(`char_import`, data.file_name, oldSelectedChar);
$("#rm_info_block").transition({ opacity: 1, duration: 1000 });
}
},
@@ -6289,12 +6300,13 @@ $(document).ready(function () {
$("#chat_import_file").click();
});
- $("#chat_import_file").on("change", function (e) {
+ $("#chat_import_file").on("change", async function (e) {
var file = e.target.files[0];
- //console.log(1);
+
if (!file) {
return;
}
+
var ext = file.name.match(/\.(\w+)$/);
if (
!ext ||
@@ -6303,33 +6315,23 @@ $(document).ready(function () {
return;
}
+ if (selected_group && file.name.endsWith('.json')) {
+ toastr.warning("Only SillyTavern's own format is supported for group chat imports. Sorry!");
+ return;
+ }
+
var format = ext[1].toLowerCase();
$("#chat_import_file_type").val(format);
- //console.log(format);
+
var formData = new FormData($("#form_import_chat").get(0));
- //console.log('/importchat entered with: '+formData);
- jQuery.ajax({
- type: "POST",
- url: "/importchat",
- data: formData,
- beforeSend: function () {
- $("#select_chat_div").html("");
- $("#load_select_chat_div").css("display", "block");
- //$('#create_button').attr('value','Creating...');
- },
- cache: false,
- contentType: false,
- processData: false,
- success: function (data) {
- //console.log(data);
- if (data.res) {
- displayPastChats();
- }
- },
- error: function (jqXHR, exception) {
- $("#create_button").removeAttr("disabled");
- },
- });
+ $("#select_chat_div").html("");
+ $("#load_select_chat_div").css("display", "block");
+
+ if (selected_group) {
+ await importGroupChat(formData);
+ } else {
+ await importCharacterChat(formData);
+ }
});
$("#rm_button_group_chats").click(function () {
diff --git a/public/scripts/context-template.js b/public/scripts/context-template.js
new file mode 100644
index 000000000..d7480b1dc
--- /dev/null
+++ b/public/scripts/context-template.js
@@ -0,0 +1,28 @@
+import {
+ callPopup,
+} from '../script.js';
+
+function openContextTemplateEditor() {
+ const editor = $('#context_editor_template .context_editor').clone();
+ $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
+ callPopup(editor.html(), 'text');
+}
+
+function copyTemplateParameter(event) {
+ const text = $(event.target).text();
+ navigator.clipboard.writeText(text);
+ const copiedMsg = document.createElement("div");
+ copiedMsg.classList.add('code-copied');
+ copiedMsg.innerText = "Copied!";
+ copiedMsg.style.top = `${event.clientY - 55}px`;
+ copiedMsg.style.left = `${event.clientX - 55}px`;
+ document.body.append(copiedMsg);
+ setTimeout(() => {
+ document.body.removeChild(copiedMsg);
+ }, 1000);
+}
+
+jQuery(() => {
+ $('#context_template_edit').on('click', openContextTemplateEditor);
+ $(document).on('pointerup', '.template_parameters_list code', copyTemplateParameter);
+})
\ No newline at end of file
diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js
index dd8c324fb..799129051 100644
--- a/public/scripts/extensions/tts/system.js
+++ b/public/scripts/extensions/tts/system.js
@@ -30,7 +30,7 @@ class SystemTtsProvider {
}
get settingsHtml() {
- if (!window.speechSynthesis) {
+ if (!('speechSynthesis' in window)) {
return "Your browser or operating system doesn't support speech synthesis";
}
@@ -81,7 +81,7 @@ class SystemTtsProvider {
// TTS Interfaces //
//#################//
fetchTtsVoiceIds() {
- if (!window.speechSynthesis) {
+ if (!('speechSynthesis' in window)) {
return [];
}
@@ -92,6 +92,10 @@ class SystemTtsProvider {
}
previewTtsVoice(voiceId) {
+ if (!('speechSynthesis' in window)) {
+ throw 'Speech synthesis API is not supported';
+ }
+
const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId);
if (!voice) {
@@ -108,11 +112,11 @@ class SystemTtsProvider {
}
async getVoice(voiceName) {
- if (!window.speechSynthesis) {
+ if (!('speechSynthesis' in window)) {
return { voice_id: null }
}
- const voices = window.speechSynthesis.getVoices();
+ const voices = speechSynthesis.getVoices();
const match = voices.find(x => x.name == voiceName);
if (!match) {
@@ -123,7 +127,7 @@ class SystemTtsProvider {
}
async generateTts(text, voiceId) {
- if (!window.speechSynthesis) {
+ if (!('speechSynthesis' in window)) {
throw 'Speech synthesis API is not supported';
}
diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js
index 171908216..65f33cc14 100644
--- a/public/scripts/group-chats.js
+++ b/public/scripts/group-chats.js
@@ -47,6 +47,7 @@ import {
select_selected_character,
cancelTtsPlay,
isMultigenEnabled,
+ displayPastChats,
} from "../script.js";
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect } from './tags.js';
@@ -292,6 +293,12 @@ async function getGroups() {
if (group.past_metadata == undefined) {
group.past_metadata = {};
}
+ if (typeof group.chat_id === 'number') {
+ group.chat_id = String(group.chat_id);
+ }
+ if (Array.isArray(group.chats) && group.chats.some(x => typeof x === 'number')) {
+ group.chats = group.chats.map(x => String(x));
+ }
}
}
}
@@ -1282,6 +1289,34 @@ export async function deleteGroupChat(groupId, chatId) {
}
}
+export async function importGroupChat(formData) {
+ await jQuery.ajax({
+ type: "POST",
+ url: "/importgroupchat",
+ data: formData,
+ beforeSend: function () {
+ },
+ cache: false,
+ contentType: false,
+ processData: false,
+ success: async function (data) {
+ if (data.res) {
+ const chatId = data.res;
+ const group = groups.find(x => x.id == selected_group);
+
+ if (group) {
+ group.chats.push(chatId);
+ await editGroup(selected_group, true, true);
+ await displayPastChats();
+ }
+ }
+ },
+ error: function () {
+ $("#create_button").removeAttr("disabled");
+ },
+ });
+}
+
export async function saveGroupBookmarkChat(groupId, name, metadata) {
const group = groups.find(x => x.id === groupId);
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 55fe7c3c6..bc35e3b05 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -18,6 +18,7 @@ import {
callPopup,
getRequestHeaders,
system_message_types,
+ replaceBiasMarkup,
} from "../script.js";
import { groups, selected_group } from "./group-chats.js";
@@ -102,7 +103,6 @@ const default_settings = {
openai_model: 'gpt-3.5-turbo',
jailbreak_system: false,
reverse_proxy: '',
- oai_breakdown: false,
};
const oai_settings = {
@@ -127,7 +127,6 @@ const oai_settings = {
openai_model: 'gpt-3.5-turbo',
jailbreak_system: false,
reverse_proxy: '',
- oai_breakdown: false,
};
let openai_setting_names;
@@ -176,8 +175,7 @@ function setOpenAIMessages(chat) {
content = `${chat[j].name}: ${content}`;
}
- // replace bias markup
- content = (content ?? '').replace(/{{(\*?.*\*?)}}/g, '');
+ content = replaceBiasMarkup(content);
// remove caret return (waste of tokens)
content = content.replace(/\r/gm, '');
@@ -460,7 +458,7 @@ async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldI
handler_instance.log();
return [
openai_msgs_tosend,
- oai_settings.oai_breakdown ? handler_instance.counts : false,
+ handler_instance.counts,
];
}
@@ -562,13 +560,19 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
const decoder = new TextDecoder();
const reader = response.body.getReader();
let getMessage = "";
+ let messageBuffer = "";
while (true) {
const { done, value } = await reader.read();
let response = decoder.decode(value);
tryParseStreamingError(response);
-
- let eventList = response.split("\n");
+
+ // ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
+ // We need to buffer chunks until we have one or more full messages (separated by double newlines)
+ messageBuffer += response;
+ let eventList = messageBuffer.split("\n\n");
+ // Last element will be an empty string or a leftover partial message
+ messageBuffer = eventList.pop();
for (let event of eventList) {
if (!event.startsWith("data"))
@@ -745,7 +749,6 @@ function loadOpenAISettings(data, settings) {
if (settings.nsfw_first !== undefined) oai_settings.nsfw_first = !!settings.nsfw_first;
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
if (settings.jailbreak_system !== undefined) oai_settings.jailbreak_system = !!settings.jailbreak_system;
- if (settings.oai_breakdown !== undefined) oai_settings.oai_breakdown = !!settings.oai_breakdown;
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
@@ -761,7 +764,6 @@ function loadOpenAISettings(data, settings) {
$('#wrap_in_quotes').prop('checked', oai_settings.wrap_in_quotes);
$('#nsfw_first').prop('checked', oai_settings.nsfw_first);
$('#jailbreak_system').prop('checked', oai_settings.jailbreak_system);
- $('#oai_breakdown').prop('checked', oai_settings.oai_breakdown);
if (settings.main_prompt !== undefined) oai_settings.main_prompt = settings.main_prompt;
if (settings.nsfw_prompt !== undefined) oai_settings.nsfw_prompt = settings.nsfw_prompt;
@@ -881,7 +883,7 @@ async function saveOpenAIPreset(name, settings) {
jailbreak_system: settings.jailbreak_system,
impersonation_prompt: settings.impersonation_prompt,
bias_preset_selected: settings.bias_preset_selected,
- oai_breakdown: settings.oai_breakdown,
+ reverse_proxy: settings.reverse_proxy,
};
const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, {
@@ -1140,12 +1142,12 @@ function onSettingsPresetChange() {
wrap_in_quotes: ['#wrap_in_quotes', 'wrap_in_quotes', true],
nsfw_first: ['#nsfw_first', 'nsfw_first', true],
jailbreak_system: ['#jailbreak_system', 'jailbreak_system', true],
- oai_breakdown: ['#oai_breakdown', 'oai_breakdown', true],
main_prompt: ['#main_prompt_textarea', 'main_prompt', false],
nsfw_prompt: ['#nsfw_prompt_textarea', 'nsfw_prompt', false],
jailbreak_prompt: ['#jailbreak_prompt_textarea', 'jailbreak_prompt', false],
impersonation_prompt: ['#impersonation_prompt_textarea', 'impersonation_prompt', false],
bias_preset_selected: ['#openai_logit_bias_preset', 'bias_preset_selected', false],
+ reverse_proxy: ['#openai_reverse_proxy', 'reverse_proxy', false],
};
for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) {
@@ -1313,16 +1315,6 @@ $(document).ready(function () {
saveSettingsDebounced();
});
- $("#oai_breakdown").on('change', function () {
- oai_settings.oai_breakdown = !!$(this).prop("checked");
- if (!oai_settings.oai_breakdown) {
- $("#token_breakdown").css('display', 'none');
- } else {
- $("#token_breakdown").css('display', 'flex');
- }
- saveSettingsDebounced();
- });
-
// auto-select a preset based on character/group name
$(document).on("click", ".character_select", function () {
const chid = $(this).attr('chid');
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index b42195897..584bbb40c 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -4,7 +4,9 @@ import {
chat,
chat_metadata,
default_avatar,
+ extractMessageBias,
getThumbnailUrl,
+ replaceBiasMarkup,
saveChatConditional,
sendSystemMessage,
system_avatar,
@@ -117,6 +119,9 @@ function sendMessageAs(_, text) {
const name = parts.shift().trim();
const mesText = parts.join('\n').trim();
+ // Messages that do nothing but set bias will be hidden from the context
+ const bias = extractMessageBias(mesText);
+ const isSystem = replaceBiasMarkup(mesText).trim().length === 0;
const character = characters.find(x => x.name === name);
let force_avatar, original_avatar;
@@ -134,11 +139,14 @@ function sendMessageAs(_, text) {
name: name,
is_user: false,
is_name: true,
- is_system: false,
+ is_system: isSystem,
send_date: humanizedDateTime(),
mes: mesText,
force_avatar: force_avatar,
original_avatar: original_avatar,
+ extra: {
+ bias: bias.trim().length ? bias : null,
+ }
};
chat.push(message);
@@ -152,16 +160,21 @@ function sendNarratorMessage(_, text) {
}
const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
+ // Messages that do nothing but set bias will be hidden from the context
+ const bias = extractMessageBias(text);
+ const isSystem = replaceBiasMarkup(text).trim().length === 0;
+
const message = {
name: name,
is_user: false,
is_name: false,
- is_system: false,
+ is_system: isSystem,
send_date: humanizedDateTime(),
mes: text.trim(),
force_avatar: system_avatar,
extra: {
type: system_message_types.NARRATOR,
+ bias: bias.trim().length ? bias : null,
},
};
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index dc84a357d..77763b536 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -216,3 +216,19 @@ export function end_trim_to_sentence(input, include_newline = false) {
return input.substring(0, last + 1).trimEnd();
}
+
+export function countOccurrences(string, character) {
+ let count = 0;
+
+ for (let i = 0; i < string.length; i++) {
+ if (string[i] === character) {
+ count++;
+ }
+ }
+
+ return count;
+}
+
+export function isOdd(number) {
+ return number % 2 !== 0;
+}
\ No newline at end of file
diff --git a/public/style.css b/public/style.css
index d5bc238d0..ff8bbfb6d 100644
--- a/public/style.css
+++ b/public/style.css
@@ -427,19 +427,6 @@ code {
justify-content: center;
}
-#token_breakdown div {
- display: flex;
- width: 100%;
- justify-content: center;
-}
-
-.token_breakdown_segment {
- min-width: 40px !important;
- border: solid 2px;
- border-radius: 5px;
-}
-
-
#loading_mes {
display: none;
width: 40px;
@@ -921,6 +908,26 @@ select {
}
+.chat_injections_list:empty {
+ width: 100%;
+ height: 100%;
+}
+
+.chat_injections_list:empty::before {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ content: "No injections";
+ font-weight: bolder;
+ width: 100%;
+ height: 100%;
+ opacity: 0.8;
+ min-height: 3rem;
+}
+
+.template_parameters_list code {
+ cursor: pointer;
+}
h3 {
margin: 10px 0;
@@ -3602,7 +3609,7 @@ label[for="extensions_autoconnect"] {
.code-copied {
position: absolute;
- z-index: 99;
+ z-index: 10000;
font-size: var(--mainFontSize);
color: var(--SmartThemeBodyColor);
background-color: var(--SmartThemeFastUIBGColor);
@@ -3744,6 +3751,10 @@ toolcool-color-picker {
flex: 1;
}
+.flex2 {
+ flex: 2;
+}
+
.flexFlowColumn {
flex-flow: column;
}
diff --git a/readme.md b/readme.md
index 8e0caa8f3..01d07057f 100644
--- a/readme.md
+++ b/readme.md
@@ -65,6 +65,8 @@ Get in touch with the developers directly:
* Character emotional expressions
* Auto-Summary of the chat history
* Sending images to chat, and the AI interpreting the content.
+ * Stable Diffusion image generation (5 chat-related presets plus 'free mode')
+ * Text-to-speech for AI response messages (via ElevenLabs, Silero, or the OS's System TTS)
## UI Extensions 🚀
@@ -76,6 +78,8 @@ Get in touch with the developers directly:
| D&D Dice | A set of 7 classic D&D dice for all your dice rolling needs.
*I used to roll the dice.
Feel the fear in my enemies' eyes* | None |

|
| Author's Note | Built-in extension that allows you to append notes that will be added to the context and steer the story and character in a specific direction. Because it's sent after the character description, it has a lot of weight. Thanks Ali឵#2222 for pitching the idea! | None |  |
| Character Backgrounds | Built-in extension to assign unique backgrounds to specific chats or groups. | None |

|
+| Stable Diffusion | Use local of cloud-based Stable Diffusion webUI API to generate images. 5 presets included ('you', 'your face', 'me', 'the story', and 'the last message'. Free mode also supported via `/sd (anything_here_)` command in the chat input bar. Most common StableDiffusion generation settings are customizable within the SillyTavern UI. | None |

|
+| Text-to-Speech | AI-generated voice will read back character messages on demand, or automatically read new messages they arrive. Supports ElevenLabs, Silero, and your device's TTS service. | None |

|
## UI/CSS/Quality of Life tweaks by RossAscends
diff --git a/server.js b/server.js
index 03e80abcd..4bfc42268 100644
--- a/server.js
+++ b/server.js
@@ -63,7 +63,9 @@ const utf8Encode = new TextEncoder();
const utf8Decode = new TextDecoder('utf-8', { ignoreBOM: true });
const commandExistsSync = require('command-exists').sync;
+const characterCardParser = require('./src/character-card-parser.js');
const config = require(path.join(process.cwd(), './config.conf'));
+
const server_port = process.env.SILLY_TAVERN_PORT || config.port;
const whitelistPath = path.join(process.cwd(), "./whitelist.txt");
@@ -145,7 +147,6 @@ const tokenizersCache = {};
function getTiktokenTokenizer(model) {
if (tokenizersCache[model]) {
- console.log('Using the cached tokenizer instance for', model);
return tokenizersCache[model];
}
@@ -913,61 +914,7 @@ async function tryReadImage(img_url, crop) {
}
async function charaRead(img_url, input_format) {
- let format;
- if (input_format === undefined) {
- if (img_url.indexOf('.webp') !== -1) {
- format = 'webp';
- } else {
- format = 'png';
- }
- } else {
- format = input_format;
- }
-
- switch (format) {
- case 'webp':
- try {
- const exif_data = await ExifReader.load(fs.readFileSync(img_url));
- let char_data;
-
- if (exif_data['UserComment']['description']) {
- let description = exif_data['UserComment']['description'];
- if (description === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) {
- description = exif_data['UserComment'].value[0];
- }
- try {
- json5.parse(description);
- char_data = description;
- } catch {
- const byteArr = description.split(",").map(Number);
- const uint8Array = new Uint8Array(byteArr);
- const char_data_string = utf8Decode.decode(uint8Array);
- char_data = char_data_string;
- }
- } else {
- console.log('No description found in EXIF data.');
- return false;
- }
- return char_data;
- }
- catch (err) {
- console.log(err);
- return false;
- }
- case 'png':
- const buffer = fs.readFileSync(img_url);
- const chunks = extract(buffer);
-
- const textChunks = chunks.filter(function (chunk) {
- return chunk.name === 'tEXt';
- }).map(function (chunk) {
- return PNGtext.decode(chunk.data);
- });
- var base64DecodedData = Buffer.from(textChunks[0].text, 'base64').toString('utf8');
- return base64DecodedData;//textChunks[0].text;
- default:
- break;
- }
+ return characterCardParser.parse(img_url, input_format);
}
app.post("/getcharacters", jsonParser, function (request, response) {
@@ -1754,6 +1701,17 @@ app.post("/exportcharacter", jsonParser, async function (request, response) {
return response.sendStatus(400);
});
+app.post("/importgroupchat", urlencodedParser, function (request, response) {
+ try {
+ const filedata = request.file;
+ const chatname = humanizedISO8601DateTime();
+ fs.copyFileSync(`./uploads/${filedata.filename}`, (`${directories.groupChats}/${chatname}.jsonl`));
+ return response.send({ res: chatname });
+ } catch (error) {
+ console.error(error);
+ return response.send({ error: true });
+ }
+});
app.post("/importchat", urlencodedParser, function (request, response) {
if (!request.body) return response.sendStatus(400);
@@ -1763,9 +1721,8 @@ app.post("/importchat", urlencodedParser, function (request, response) {
let avatar_url = (request.body.avatar_url).replace('.png', '');
let ch_name = request.body.character_name;
if (filedata) {
-
if (format === 'json') {
- fs.readFile('./uploads/' + filedata.filename, 'utf8', (err, data) => {
+ fs.readFile(`./uploads/${filedata.filename}`, 'utf8', (err, data) => {
if (err) {
console.log(err);
@@ -1782,7 +1739,6 @@ app.post("/importchat", urlencodedParser, function (request, response) {
user_name: 'You',
character_name: ch_name,
create_date: humanizedISO8601DateTime(),
-
},
...history.msgs.map(
(message) => ({
@@ -1803,7 +1759,7 @@ app.post("/importchat", urlencodedParser, function (request, response) {
const errors = [];
newChats.forEach(chat => fs.writeFile(
- chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + ' imported.jsonl',
+ `${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`,
chat.map(JSON.stringify).join('\n'),
'utf8',
(err) => err ?? errors.push(err)
@@ -1832,8 +1788,7 @@ app.post("/importchat", urlencodedParser, function (request, response) {
let jsonData = json5.parse(line);
if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
- //console.log(humanizedISO8601DateTime()+':/importchat copying chat as '+ch_name+' - '+humanizedISO8601DateTime()+'.jsonl');
- fs.copyFile('./uploads/' + filedata.filename, chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + '.jsonl', (err) => { //added character name and replaced Date.now() with humanizedISO8601DateTime
+ fs.copyFile(`./uploads/${filedata.filename}`, (`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`), (err) => {
if (err) {
response.send({ error: true });
return console.log(err);
@@ -1849,9 +1804,7 @@ app.post("/importchat", urlencodedParser, function (request, response) {
rl.close();
});
}
-
}
-
});
app.post('/importworldinfo', urlencodedParser, (request, response) => {
@@ -1919,7 +1872,7 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => {
const crop = tryParse(request.query.crop);
let rawImg = await jimp.read(pathToUpload);
- if (typeof crop == 'object') {
+ if (typeof crop == 'object' && [crop.x, crop.y, crop.width, crop.height].every(x => typeof x === 'number')) {
rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height);
}
@@ -2746,8 +2699,12 @@ const setupTasks = async function () {
}
if (listen && !config.whitelistMode && !config.basicAuthMode) {
- console.error('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.');
- process.exit(1);
+ if (config.securityOverride)
+ console.warn("Security has been override. If it's not a trusted network, change the settings.");
+ else {
+ console.error('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.');
+ process.exit(1);
+ }
}
if (true === cliArguments.ssl)
diff --git a/src/character-card-parser.js b/src/character-card-parser.js
new file mode 100644
index 000000000..e97a66bf0
--- /dev/null
+++ b/src/character-card-parser.js
@@ -0,0 +1,72 @@
+const fs = require('fs');
+const json5 = require('json5');
+const ExifReader = require('exifreader');
+
+const extract = require('png-chunks-extract');
+const PNGtext = require('png-chunk-text');
+
+const utf8Decode = new TextDecoder('utf-8', { ignoreBOM: true });
+
+const parse = async (cardUrl, format) => {
+ let fileFormat;
+ if (format === undefined) {
+ if (cardUrl.indexOf('.webp') !== -1)
+ fileFormat = 'webp';
+ else
+ fileFormat = 'png';
+ }
+ else
+ fileFormat = format;
+
+ switch (fileFormat) {
+ case 'webp':
+ try {
+ const exif_data = await ExifReader.load(fs.readFileSync(cardUrl));
+ let char_data;
+
+ if (exif_data['UserComment']['description']) {
+ let description = exif_data['UserComment']['description'];
+ if (description === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) {
+ description = exif_data['UserComment'].value[0];
+ }
+
+ try {
+ json5.parse(description);
+ char_data = description;
+ } catch {
+ const byteArr = description.split(",").map(Number);
+ const uint8Array = new Uint8Array(byteArr);
+ const char_data_string = utf8Decode.decode(uint8Array);
+ char_data = char_data_string;
+ }
+ }
+ else {
+ console.log('No description found in EXIF data.');
+ return false;
+ }
+
+ return char_data;
+ }
+ catch (err) {
+ console.log(err);
+ return false;
+ }
+ case 'png':
+ const buffer = fs.readFileSync(cardUrl);
+ const chunks = extract(buffer);
+
+ const textChunks = chunks.filter(function (chunk) {
+ return chunk.name === 'tEXt';
+ }).map(function (chunk) {
+ return PNGtext.decode(chunk.data);
+ });
+
+ return Buffer.from(textChunks[0].text, 'base64').toString('utf8');
+ default:
+ break;
+ }
+};
+
+module.exports = {
+ parse: parse
+};