Merge pull request #1290 from SillyTavern/staging

Staging
This commit is contained in:
Cohee 2023-10-26 18:44:18 +03:00 committed by GitHub
commit b4c7bb1f7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 3751 additions and 7436 deletions

View File

@ -1,8 +1,8 @@
[English](readme.md) | 中文 [English](readme.md) | 中文
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/8c41a061-7f72-4d2b-9d54-e6d058209e7b) ![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
移动设备界面友好多种人工智能服务或模型支持KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale类似 Galgame 的 老 婆 模 式Horde SD文本系统语音生成世界信息Lorebooks可定制的界面自动翻译和比你所需要的更多的 Prompt。附带扩展服务支持文本绘画生成与语音生成和基于向量数据库 ChromaDB 的聊天信息总结。 移动设备界面友好多种人工智能服务或模型支持KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale类似 Galgame 的 老 婆 模 式Horde SD文本系统语音生成世界信息Lorebooks可定制的界面自动翻译和比你所需要的更多的 Prompt。附带扩展服务支持文本绘画生成与语音生成和基于向量数据库 的聊天信息总结。
基于 TavernAI 1.2.8 的分叉版本 基于 TavernAI 1.2.8 的分叉版本
@ -81,7 +81,6 @@ SillyTavern 支持扩展服务,一些额外的人工智能模块可通过 [Sil
* 在聊天窗口发送图片,并由人工智能解释图片内容 * 在聊天窗口发送图片,并由人工智能解释图片内容
* 文本图像生成5 预设,以及 "自由模式" * 文本图像生成5 预设,以及 "自由模式"
* 聊天信息的文字转语音(通过 ElevenLabs、Silero 或操作系统的语音生成) * 聊天信息的文字转语音(通过 ElevenLabs、Silero 或操作系统的语音生成)
* ChromaDB 向量数据库,用于更智能的聊天 Prompt
扩展服务的完整功能介绍和使用教程,请参阅 [Docs](https://docs.sillytavern.app/extras/extensions/)。 扩展服务的完整功能介绍和使用教程,请参阅 [Docs](https://docs.sillytavern.app/extras/extensions/)。

15
.github/readme.md vendored
View File

@ -1,15 +1,19 @@
English | [中文](readme-zh_cn.md) English | [中文](readme-zh_cn.md)
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/8c41a061-7f72-4d2b-9d54-e6d058209e7b) ![SillyTavern-Banner](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
Mobile-friendly, Multi-API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale), VN-like Waifu Mode, Horde SD, System TTS, WorldInfo (lorebooks), customizable UI, auto-translate, and more prompt options than you'd ever want or need. Optional Extras server for more SD/TTS options + ChromaDB/Summarize. Mobile-friendly layout, Multi-API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale), VN-like Waifu Mode, Stable Diffusion, TTS, WorldInfo (lorebooks), customizable UI, auto-translate, and more prompt options than you'd ever want or need + ability to install third-party extensions.
Based on a fork of TavernAI 1.2.8 Based on a fork of [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8
## Important news!
1. We have created a [Documentation website](https://docs.sillytavern.app/) to answer most of your questions and help you get started.
2. Missing extensions after the update? Since the 1.10.6 release version, most of the previously built-in extensions have been converted to downloadable add-ons. You can download them via the built-in "Download Extensions and Assets" menu in the extensions panel (stacked blocks icon in the top bar).
### Brought to you by Cohee, RossAscends, and the SillyTavern community ### Brought to you by Cohee, RossAscends, and the SillyTavern community
NOTE: We have created a [Documentation website](https://docs.sillytavern.app/) to answer most of your questions and help you get started.
### What is SillyTavern or TavernAI? ### What is SillyTavern or TavernAI?
SillyTavern is a user interface you can install on your computer (and Android phones) that allows you to interact with text generation AIs and chat/roleplay with characters you or the community create. SillyTavern is a user interface you can install on your computer (and Android phones) that allows you to interact with text generation AIs and chat/roleplay with characters you or the community create.
@ -80,7 +84,6 @@ SillyTavern has extensibility support, with some additional AI modules hosted vi
* Sending images to chat, and the AI interpreting the content * Sending images to chat, and the AI interpreting the content
* Stable Diffusion image generation (5 chat-related presets plus 'free mode') * 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) * Text-to-speech for AI response messages (via ElevenLabs, Silero, or the OS's System TTS)
* ChromaDB vector storage for smarter chat prompt formatting
A full list of included extensions and tutorials on how to use them can be found in the [Docs](https://docs.sillytavern.app/extras/extensions/). A full list of included extensions and tutorials on how to use them can be found in the [Docs](https://docs.sillytavern.app/extras/extensions/).

View File

@ -70,7 +70,7 @@
"#@markdown * ckpt/sd15 - base SD 1.5\n", "#@markdown * ckpt/sd15 - base SD 1.5\n",
"#@markdown * stabilityai/stable-diffusion-2-1-base - base SD 2.1\n", "#@markdown * stabilityai/stable-diffusion-2-1-base - base SD 2.1\n",
"extras_enable_chromadb = True #@param {type:\"boolean\"}\n", "extras_enable_chromadb = True #@param {type:\"boolean\"}\n",
"#@markdown Enables ChromaDB for Infinity Context plugin\n", "#@markdown Enables ChromaDB module\n",
"\n", "\n",
"import subprocess\n", "import subprocess\n",
"import secrets\n", "import secrets\n",

View File

@ -10,6 +10,7 @@ const listen = true; // If true, Can be access from other device or PC. otherwis
const allowKeysExposure = false; // If true, private API keys could be fetched to the frontend. const allowKeysExposure = false; // If true, private API keys could be fetched to the frontend.
const skipContentCheck = false; // If true, no new default content will be delivered to you. const skipContentCheck = false; // If true, no new default content will be delivered to you.
const thumbnailsQuality = 95; // Quality of thumbnails. 0-100 const thumbnailsQuality = 95; // Quality of thumbnails. 0-100
const disableChatBackup = false; // Disables the backup of chat logs to the /backups folder
// If true, Allows insecure settings for listen, whitelist, and authentication. // If true, Allows insecure settings for listen, whitelist, and authentication.
// Change this setting only on "trusted networks". Do not change this value unless you are aware of the issues that can arise from changing this setting and configuring a insecure setting. // Change this setting only on "trusted networks". Do not change this value unless you are aware of the issues that can arise from changing this setting and configuring a insecure setting.
@ -26,6 +27,8 @@ const extras = {
captioningModel: 'Xenova/vit-gpt2-image-captioning', captioningModel: 'Xenova/vit-gpt2-image-captioning',
// Feature extraction model. HuggingFace ID of a model in ONNX format. // Feature extraction model. HuggingFace ID of a model in ONNX format.
embeddingModel: 'Xenova/all-mpnet-base-v2', embeddingModel: 'Xenova/all-mpnet-base-v2',
// GPT-2 text generation model. HuggingFace ID of a model in ONNX format.
promptExpansionModel: 'Cohee/fooocus_expansion-onnx',
}; };
// Request overrides for additional headers // Request overrides for additional headers
@ -49,4 +52,5 @@ module.exports = {
requestOverrides, requestOverrides,
thumbnailsQuality, thumbnailsQuality,
extras, extras,
disableChatBackup,
}; };

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "sillytavern", "name": "sillytavern",
"version": "1.10.5", "version": "1.10.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sillytavern", "name": "sillytavern",
"version": "1.10.5", "version": "1.10.6",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

View File

@ -47,7 +47,7 @@
"type": "git", "type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git" "url": "https://github.com/SillyTavern/SillyTavern.git"
}, },
"version": "1.10.5", "version": "1.10.6",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"start-multi": "node server.js --disableCsrf", "start-multi": "node server.js --disableCsrf",

View File

@ -0,0 +1 @@
Put blip audio files here

View File

@ -0,0 +1 @@
Put live2d model folders here

View File

@ -1,6 +1,11 @@
{ {
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"chat_start": "",
"example_separator": "", "example_separator": "",
"chat_start": "",
"always_force_name2": false,
"trim_sentences": false,
"include_newline": false,
"custom_stopping_strings": "[\"\\n\"]",
"custom_stopping_strings_macro": true,
"name": "Adventure" "name": "Adventure"
} }

View File

@ -103,39 +103,39 @@ input.extension_missing[type="checkbox"] {
} }
/** LEFT COLUMN **/ /** LEFT COLUMN **/
#extensions_settings>.expression_settings { #extensions_settings>#assets_ui {
order: 1; order: 1;
} }
#extensions_settings>.background_settings { #extensions_settings>.expression_settings {
order: 2; order: 2;
} }
#extensions_settings>.sd_settings { #extensions_settings>.background_settings {
order: 3; order: 3;
} }
#extensions_settings>#tts_settings { #extensions_settings>.sd_settings {
order: 4; order: 4;
} }
#extensions_settings>#rvc_settings { #extensions_settings>#tts_settings {
order: 5; order: 5;
} }
#extensions_settings>.objective-settings { #extensions_settings>#rvc_settings {
order: 6; order: 6;
} }
#extensions_settings>#speech_recognition_settings { #extensions_settings>.objective-settings {
order: 7; order: 7;
} }
#extensions_settings>#audio_settings { #extensions_settings>#speech_recognition_settings {
order: 8; order: 8;
} }
#extensions_settings>#assets_ui { #extensions_settings>#audio_settings {
order: 9; order: 9;
} }

View File

@ -188,7 +188,8 @@
#showRawPrompt, #showRawPrompt,
#copyPromptToClipboard, #copyPromptToClipboard,
#groupCurrentMemberPopoutButton { #groupCurrentMemberPopoutButton,
#summaryExtensionPopoutButton {
display: none; display: none;
} }
@ -292,7 +293,7 @@
display: none; display: none;
} }
#bg_menu_content { .bg_list {
width: unset; width: unset;
} }
} }
@ -444,4 +445,4 @@
#horde_model { #horde_model {
height: unset; height: unset;
} }
} }

View File

@ -78,6 +78,7 @@
#rm_group_members:empty { #rm_group_members:empty {
width: 100%; width: 100%;
padding: 0.5em 0;
} }
#rm_group_members:empty::before { #rm_group_members:empty::before {
@ -226,4 +227,5 @@
.group_member .avatar { .group_member .avatar {
flex-shrink: 0; flex-shrink: 0;
} flex-basis: auto;
}

View File

@ -6,6 +6,12 @@
color: var(--fullred); color: var(--fullred);
} }
.highlighted {
color: black;
background-color: yellow;
text-shadow: none !important;
}
.m-t-0 { .m-t-0 {
margin-top: 0; margin-top: 0;
} }

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;
} }
@ -171,4 +170,4 @@
-1px 1px 0px black, -1px 1px 0px black,
1px -1px 0px black; 1px -1px 0px black;
opacity: 1; opacity: 1;
} }

View File

@ -328,8 +328,7 @@ body.movingUI .drawer-content,
body.movingUI #expression-holder, body.movingUI #expression-holder,
body.movingUI .zoomed_avatar, body.movingUI .zoomed_avatar,
body.movingUI .draggable, body.movingUI .draggable,
body.movingUI #floatingPrompt, body.movingUI #floatingPrompt {
body.movingUI #groupMemberListPopout {
resize: both; resize: both;
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,16 @@ function debouncePromise(func, delay) {
}; };
} }
const DEFAULT_DEPTH = 4;
/**
* @enum {number}
*/
export const INJECTION_POSITION ={
RELATIVE: 0,
ABSOLUTE: 1,
}
/** /**
* Register migrations for the prompt manager when settings are loaded or an Open AI preset is loaded. * Register migrations for the prompt manager when settings are loaded or an Open AI preset is loaded.
*/ */
@ -60,7 +70,7 @@ const registerPromptManagerMigration = () => {
* Represents a prompt. * Represents a prompt.
*/ */
class Prompt { class Prompt {
identifier; role; content; name; system_prompt; position; identifier; role; content; name; system_prompt; position; injection_position; injection_depth;
/** /**
* Create a new Prompt instance. * Create a new Prompt instance.
@ -72,14 +82,18 @@ class Prompt {
* @param {string} param0.name - The name of the prompt. * @param {string} param0.name - The name of the prompt.
* @param {boolean} param0.system_prompt - Indicates if the prompt is a system prompt. * @param {boolean} param0.system_prompt - Indicates if the prompt is a system prompt.
* @param {string} param0.position - The position of the prompt in the prompt list. * @param {string} param0.position - The position of the prompt in the prompt list.
* @param {number} param0.injection_position - The insert position of the prompt.
* @param {number} param0.injection_depth - The depth of the prompt in the chat.
*/ */
constructor({ identifier, role, content, name, system_prompt, position } = {}) { constructor({ identifier, role, content, name, system_prompt, position, injection_depth, injection_position } = {}) {
this.identifier = identifier; this.identifier = identifier;
this.role = role; this.role = role;
this.content = content; this.content = content;
this.name = name; this.name = name;
this.system_prompt = system_prompt; this.system_prompt = system_prompt;
this.position = position; this.position = position;
this.injection_depth = injection_depth;
this.injection_position = injection_position;
} }
} }
@ -381,6 +395,8 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value = prompt.name; document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value = prompt.name;
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value = 'system'; document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value = 'system';
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value = prompt.content; document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value = prompt.content;
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value = prompt.injection_position ?? 0;
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value = prompt.injection_depth ?? DEFAULT_DEPTH;
} }
// Append prompt to selected character // Append prompt to selected character
@ -673,6 +689,8 @@ PromptManagerModule.prototype.updatePromptWithPromptEditForm = function (prompt)
prompt.name = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value; prompt.name = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value;
prompt.role = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value; prompt.role = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value;
prompt.content = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value; prompt.content = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value;
prompt.injection_position = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value);
prompt.injection_depth = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value);
} }
/** /**
@ -1085,10 +1103,14 @@ PromptManagerModule.prototype.loadPromptIntoEditForm = function (prompt) {
const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'); const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name');
const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'); const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role');
const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'); const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');
const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');
nameField.value = prompt.name ?? ''; nameField.value = prompt.name ?? '';
roleField.value = prompt.role ?? ''; roleField.value = prompt.role ?? '';
promptField.value = prompt.content ?? ''; promptField.value = prompt.content ?? '';
injectionPositionField.value = prompt.injection_position ?? INJECTION_POSITION.RELATIVE;
injectionDepthField.value = prompt.injection_depth ?? DEFAULT_DEPTH;
const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset'); const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset');
if (true === prompt.system_prompt) { if (true === prompt.system_prompt) {
@ -1152,10 +1174,14 @@ PromptManagerModule.prototype.clearEditForm = function () {
const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'); const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name');
const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'); const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role');
const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'); const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');
const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');
nameField.value = ''; nameField.value = '';
roleField.selectedIndex = 0; roleField.selectedIndex = 0;
promptField.value = ''; promptField.value = '';
injectionPositionField.selectedIndex = 0;
injectionDepthField.value = DEFAULT_DEPTH;
roleField.disabled = false; roleField.disabled = false;
} }
@ -1435,13 +1461,18 @@ PromptManagerModule.prototype.renderPromptManagerListItems = function () {
} }
const encodedName = escapeHtml(prompt.name); const encodedName = escapeHtml(prompt.name);
const isSystemPrompt = !prompt.marker && prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE;
const isUserPrompt = !prompt.marker && !prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE;
const isInjectionPrompt = !prompt.marker && prompt.injection_position === INJECTION_POSITION.ABSOLUTE;
listItemHtml += ` listItemHtml += `
<li class="${prefix}prompt_manager_prompt ${draggableClass} ${enabledClass} ${markerClass}" data-pm-identifier="${prompt.identifier}"> <li class="${prefix}prompt_manager_prompt ${draggableClass} ${enabledClass} ${markerClass}" data-pm-identifier="${prompt.identifier}">
<span class="${prefix}prompt_manager_prompt_name" data-pm-name="${encodedName}"> <span class="${prefix}prompt_manager_prompt_name" data-pm-name="${encodedName}">
${prompt.marker ? '<span class="fa-solid fa-thumb-tack" title="Marker"></span>' : ''} ${prompt.marker ? '<span class="fa-solid fa-thumb-tack" title="Marker"></span>' : ''}
${!prompt.marker && prompt.system_prompt ? '<span class="fa-solid fa-square-poll-horizontal" title="Global Prompt"></span>' : ''} ${isSystemPrompt ? '<span class="fa-solid fa-square-poll-horizontal" title="Global Prompt"></span>' : ''}
${!prompt.marker && !prompt.system_prompt ? '<span class="fa-solid fa-user" title="User Prompt"></span>' : ''} ${isUserPrompt ? '<span class="fa-solid fa-user" title="User Prompt"></span>' : ''}
${isInjectionPrompt ? `<span class="fa-solid fa-syringe" title="In-Chat Injection"></span>` : ''}
${this.isPromptInspectionAllowed(prompt) ? `<a class="prompt-manager-inspect-action">${encodedName}</a>` : encodedName} ${this.isPromptInspectionAllowed(prompt) ? `<a class="prompt-manager-inspect-action">${encodedName}</a>` : encodedName}
${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${prompt.injection_depth}</small>` : ''}
</span> </span>
<span> <span>
<span class="prompt_manager_prompt_controls"> <span class="prompt_manager_prompt_controls">

View File

@ -18,6 +18,8 @@ import {
getThumbnailUrl, getThumbnailUrl,
selectCharacterById, selectCharacterById,
eventSource, eventSource,
menu_type,
substituteParams,
} from "../script.js"; } from "../script.js";
import { import {
@ -31,7 +33,7 @@ import {
SECRET_KEYS, SECRET_KEYS,
secret_state, secret_state,
} from "./secrets.js"; } from "./secrets.js";
import { debounce, delay, getStringHash, isUrlOrAPIKey, waitUntilCondition } from "./utils.js"; import { debounce, delay, getStringHash, isValidUrl, waitUntilCondition } from "./utils.js";
import { chat_completion_sources, oai_settings } from "./openai.js"; import { chat_completion_sources, oai_settings } from "./openai.js";
import { getTokenCount } from "./tokenizers.js"; import { getTokenCount } from "./tokenizers.js";
@ -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;
@ -394,7 +398,7 @@ function RA_autoconnect(PrevApi) {
if (online_status === "no_connection" && LoadLocalBool('AutoConnectEnabled')) { if (online_status === "no_connection" && LoadLocalBool('AutoConnectEnabled')) {
switch (main_api) { switch (main_api) {
case 'kobold': case 'kobold':
if (api_server && isUrlOrAPIKey(api_server)) { if (api_server && isValidUrl(api_server)) {
$("#api_button").click(); $("#api_button").click();
} }
break; break;
@ -404,7 +408,7 @@ function RA_autoconnect(PrevApi) {
} }
break; break;
case 'textgenerationwebui': case 'textgenerationwebui':
if (api_server_textgenerationwebui && isUrlOrAPIKey(api_server_textgenerationwebui)) { if (api_server_textgenerationwebui && isValidUrl(api_server_textgenerationwebui)) {
$("#api_button_textgenerationwebui").click(); $("#api_button_textgenerationwebui").click();
} }
break; break;
@ -897,6 +901,9 @@ export function initRossMods() {
//Regenerate if user swipes on the last mesage in chat //Regenerate if user swipes on the last mesage in chat
document.addEventListener('swiped-left', function (e) { document.addEventListener('swiped-left', function (e) {
if (power_user.gestures === false) {
return
}
var SwipeButR = $('.swipe_right:last'); var SwipeButR = $('.swipe_right:last');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes'); var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) { if (SwipeTargetMesClassParent !== null) {
@ -906,6 +913,9 @@ export function initRossMods() {
} }
}); });
document.addEventListener('swiped-right', function (e) { document.addEventListener('swiped-right', function (e) {
if (power_user.gestures === false) {
return
}
var SwipeButL = $('.swipe_left:last'); var SwipeButL = $('.swipe_left:last');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes'); var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) { if (SwipeTargetMesClassParent !== null) {

View File

@ -235,6 +235,7 @@ function loadSettings() {
chat_metadata[metadata_keys.depth] = chat_metadata[metadata_keys.depth] ?? extension_settings.note.defaultDepth ?? DEFAULT_DEPTH; chat_metadata[metadata_keys.depth] = chat_metadata[metadata_keys.depth] ?? extension_settings.note.defaultDepth ?? DEFAULT_DEPTH;
$('#extension_floating_prompt').val(chat_metadata[metadata_keys.prompt]); $('#extension_floating_prompt').val(chat_metadata[metadata_keys.prompt]);
$('#extension_floating_interval').val(chat_metadata[metadata_keys.interval]); $('#extension_floating_interval').val(chat_metadata[metadata_keys.interval]);
$('#extension_floating_allow_wi_scan').prop('checked', extension_settings.note.allowWIScan ?? false);
$('#extension_floating_depth').val(chat_metadata[metadata_keys.depth]); $('#extension_floating_depth').val(chat_metadata[metadata_keys.depth]);
$(`input[name="extension_floating_position"][value="${chat_metadata[metadata_keys.position]}"]`).prop('checked', true); $(`input[name="extension_floating_position"][value="${chat_metadata[metadata_keys.position]}"]`).prop('checked', true);
@ -389,6 +390,11 @@ function onChatChanged() {
$('#extension_floating_default_token_counter').text(tokenCounter3); $('#extension_floating_default_token_counter').text(tokenCounter3);
} }
function onAllowWIScanCheckboxChanged() {
extension_settings.note.allowWIScan = !!$(this).prop('checked');
updateSettings();
}
/** /**
* Inject author's note options and setup event listeners. * Inject author's note options and setup event listeners.
*/ */
@ -402,6 +408,7 @@ export function initAuthorsNote() {
$('#extension_floating_default').on('input', onExtensionFloatingDefaultInput); $('#extension_floating_default').on('input', onExtensionFloatingDefaultInput);
$('#extension_default_depth').on('input', onDefaultDepthInput); $('#extension_default_depth').on('input', onDefaultDepthInput);
$('#extension_default_interval').on('input', onDefaultIntervalInput); $('#extension_default_interval').on('input', onDefaultIntervalInput);
$('#extension_floating_allow_wi_scan').on('input', onAllowWIScanCheckboxChanged);
$('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput); $('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput);
$('input[name="extension_default_position"]').on('change', onDefaultPositionInput); $('input[name="extension_default_position"]').on('change', onDefaultPositionInput);
$('input[name="extension_floating_char_position"]').on('change', onExtensionFloatingCharPositionInput); $('input[name="extension_floating_char_position"]').on('change', onExtensionFloatingCharPositionInput);

View File

@ -0,0 +1,488 @@
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl } from "../script.js";
import { saveMetadataDebounced } from "./extensions.js";
import { registerSlashCommand } from "./slash-commands.js";
import { stringFormat } from "./utils.js";
const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
/**
* Sets the background for the current chat and adds it to the list of custom backgrounds.
* @param {{url: string, path:string}} backgroundInfo
*/
function forceSetBackground(backgroundInfo) {
saveBackgroundMetadata(backgroundInfo.url);
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();
}
async function onChatChanged() {
if (hasCustomBackground()) {
setCustomBackground();
}
else {
unsetCustomBackground();
}
getChatBackgroundsList();
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) {
return `backgrounds/${fileUrl}`;
}
function highlightLockedBackground() {
$('.bg_example').removeClass('locked');
const lockedBackground = chat_metadata[BG_METADATA_KEY];
if (!lockedBackground) {
return;
}
$(`.bg_example`).each(function () {
const url = $(this).data('url');
if (url === lockedBackground) {
$(this).addClass('locked');
}
});
}
function onLockBackgroundClick(e) {
e.stopPropagation();
const chatName = getCurrentChatId();
if (!chatName) {
toastr.warning('Select a chat to lock the background for it');
return;
}
const relativeBgImage = getUrlParameter(this);
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
highlightLockedBackground();
}
function onUnlockBackgroundClick(e) {
e.stopPropagation();
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
}
function hasCustomBackground() {
return chat_metadata[BG_METADATA_KEY];
}
function saveBackgroundMetadata(file) {
chat_metadata[BG_METADATA_KEY] = file;
saveMetadataDebounced();
}
function removeBackgroundMetadata() {
delete chat_metadata[BG_METADATA_KEY];
saveMetadataDebounced();
}
function setCustomBackground() {
const file = chat_metadata[BG_METADATA_KEY];
// bg already set
if (document.getElementById("bg_custom").style.backgroundImage == file) {
return;
}
$("#bg_custom").css("background-image", file);
}
function unsetCustomBackground() {
$("#bg_custom").css("background-image", 'none');
}
function onSelectBackgroundClick() {
const isCustom = $(this).attr('custom') === 'true';
const relativeBgImage = getUrlParameter(this);
// if clicked on upload button
if (!relativeBgImage) {
return;
}
// Automatically lock the background if it's custom or other background is locked
if (hasCustomBackground() || isCustom) {
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
highlightLockedBackground();
} else {
highlightLockedBackground();
}
const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage;
// Custom background is set. Do not override the layer below
if (customBg !== 'none') {
return;
}
const bgFile = $(this).attr("bgfile");
const backgroundUrl = getBackgroundPath(bgFile);
// Fetching to browser memory to reduce flicker
fetch(backgroundUrl).then(() => {
$("#bg1").css("background-image", relativeBgImage);
setBackground(bgFile);
}).catch(() => {
console.log('Background could not be set: ' + backgroundUrl);
});
}
async function onCopyToSystemBackgroundClick(e) {
e.stopPropagation();
const bgNames = await getNewBackgroundName(this);
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');
return;
}
const fileExtension = oldBg.split('.').pop();
const fileNameBase = isCustom ? oldBg.split('/').pop() : oldBg;
const oldBgExtensionless = fileNameBase.replace(`.${fileExtension}`, '');
const newBgExtensionless = await callPopup('<h3>Enter new background name:</h3>', 'input', oldBgExtensionless);
if (!newBgExtensionless) {
console.debug('no new_bg_extensionless');
return;
}
const newBg = `${newBgExtensionless}.${fileExtension}`;
if (oldBgExtensionless === newBgExtensionless) {
console.debug('new_bg === old_bg');
return;
}
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', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(data),
cache: 'no-cache',
});
if (response.ok) {
await getBackgrounds();
highlightNewBackground(bgNames.newBg);
} else {
toastr.warning('Failed to rename background');
}
}
async function onDeleteBackgroundClick(e) {
e.stopPropagation();
const bgToDelete = $(this).closest('.bg_example');
const url = bgToDelete.data('url');
const isCustom = bgToDelete.attr('custom') === 'true';
const confirm = await callPopup("<h3>Delete the background?</h3>", 'confirm');
const bg = bgToDelete.attr('bgfile');
if (confirm) {
// 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 nextBg = bgToDelete.next(siblingSelector);
const prevBg = bgToDelete.prev(siblingSelector);
const anyBg = $(siblingSelector);
if (nextBg.length > 0) {
nextBg.trigger('click');
} else if (prevBg.length > 0) {
prevBg.trigger('click');
} else {
$(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click');
}
bgToDelete.remove();
if (url === chat_metadata[BG_METADATA_KEY]) {
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
}
if (isCustom) {
getChatBackgroundsList();
saveMetadataDebounced();
}
}
}
const autoBgPrompt = `Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}`;
async function autoBackgroundCommand() {
/** @type {HTMLElement[]} */
const bgTitles = Array.from(document.querySelectorAll('#bg_menu_content .BGSampleTitle'));
const options = bgTitles.map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return;
}
const list = options.map(option => `- ${option.text}`).join('\n');
const prompt = stringFormat(autoBgPrompt, list);
const reply = await generateQuietPrompt(prompt, false, false);
const fuse = new Fuse(options, { keys: ['text'] });
const bestMatch = fuse.search(reply, { limit: 1 });
if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.');
return;
}
console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click();
}
export async function getBackgrounds() {
const response = await fetch("/getbackgrounds", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
"": "",
}),
});
if (response.ok === true) {
const getData = await response.json();
//background = getData;
//console.log(getData.length);
$("#bg_menu_content").children('div').remove();
for (const bg of getData) {
const template = getBackgroundFromTemplate(bg, false);
$("#bg_menu_content").append(template);
}
}
}
/**
* Gets the URL of the background
* @param {Element} block
* @returns {string} URL of the background
*/
function getUrlParameter(block) {
return $(block).closest(".bg_example").data("url");
}
/**
* Instantiates a background template
* @param {string} bg Path to background
* @param {boolean} isCustom Whether the background is custom
* @returns {JQuery<HTMLElement>} Background template
*/
function getBackgroundFromTemplate(bg, isCustom) {
const template = $('#background_template .bg_example').clone();
const thumbPath = isCustom ? bg : getThumbnailUrl('bg', 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('custom', String(isCustom));
template.data('url', url);
template.css('background-image', `url('${thumbPath}')`);
template.find('.BGSampleTitle').text(friendlyTitle);
return template;
}
async function setBackground(bg) {
jQuery.ajax({
type: "POST", //
url: "/setbackground", //
data: JSON.stringify({
bg: bg,
}),
beforeSend: function () {
},
cache: false,
dataType: "json",
contentType: "application/json",
//processData: false,
success: function (html) { },
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
}
async function delBackground(bg) {
const response = await fetch("/delbackground", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
bg: bg,
}),
});
}
function onBackgroundUploadSelected() {
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);
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);
},
});
}
/**
* @param {string} bg
*/
function highlightNewBackground(bg) {
const newBg = $(`.bg_example[bgfile="${bg}"]`);
const scrollOffset = newBg.offset().top - newBg.parent().offset().top;
$('#Backgrounds').scrollTop(scrollOffset);
newBg.addClass('flash animated');
setTimeout(() => newBg.removeClass('flash animated'), 2000);
}
function onBackgroundFilterInput() {
const filterValue = String($(this).val()).toLowerCase();
$("#bg_menu_content > div").each(function () {
const $bgContent = $(this);
if ($bgContent.attr("title").toLowerCase().includes(filterValue)) {
$bgContent.show();
} else {
$bgContent.hide();
}
});
}
export function initBackgrounds() {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
$(document).on("click", '.bg_example', onSelectBackgroundClick);
$(document).on('click', '.bg_example_lock', onLockBackgroundClick);
$(document).on('click', '.bg_example_unlock', onUnlockBackgroundClick);
$(document).on('click', '.bg_example_edit', onRenameBackgroundClick);
$(document).on("click", '.bg_example_cross', onDeleteBackgroundClick);
$(document).on("click", '.bg_example_copy', onCopyToSystemBackgroundClick);
$('#auto_background').on("click", autoBackgroundCommand);
$("#add_bg_button").on('change', onBackgroundUploadSelected);
$("#bg-filter").on("input", onBackgroundFilterInput);
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], " locks a background for the currently selected chat", true, true);
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' unlocks a background for the currently selected chat', true, true);
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' automatically changes the background based on the chat context using the AI request prompt', true, true);
}

View File

@ -1,4 +1,4 @@
import { characters, getCharacters, handleDeleteCharacter, callPopup } from "../../../script.js"; import { characters, getCharacters, handleDeleteCharacter, callPopup } from "../script.js";
let is_bulk_edit = false; let is_bulk_edit = false;
@ -64,23 +64,6 @@ async function onDeleteButtonClick() {
} }
} }
/**
* Adds the bulk edit and delete buttons to the UI.
*/
function addButtons() {
const editButton = $(
"<i id='bulkEditButton' class='fa-solid fa-edit menu_button bulkEditButton' title='Bulk edit characters'></i>"
);
const deleteButton = $(
"<i id='bulkDeleteButton' class='fa-solid fa-trash menu_button bulkDeleteButton' title='Bulk delete characters' style='display: none;'></i>"
);
$("#charListGridToggle").after(editButton, deleteButton);
$("#bulkEditButton").on("click", onEditButtonClick);
$("#bulkDeleteButton").on("click", onDeleteButtonClick);
}
/** /**
* Enables bulk selection by adding a checkbox next to each character. * Enables bulk selection by adding a checkbox next to each character.
*/ */
@ -111,7 +94,7 @@ function disableBulkSelect() {
/** /**
* Entry point that runs on page load. * Entry point that runs on page load.
*/ */
jQuery(async () => { jQuery(() => {
addButtons(); $("#bulkEditButton").on("click", onEditButtonClick);
// loadSettings(); $("#bulkDeleteButton").on("click", onDeleteButtonClick);
}); });

View File

@ -1,19 +1,17 @@
import { import {
chat_metadata, chat_metadata,
substituteParams,
this_chid,
eventSource, eventSource,
event_types, event_types,
saveSettingsDebounced, saveSettingsDebounced,
this_chid, } from "../script.js";
} from "../../../script.js"; import { extension_settings, saveMetadataDebounced } from "./extensions.js"
import { selected_group } from "../../group-chats.js"; import { selected_group } from "./group-chats.js";
import { extension_settings, saveMetadataDebounced } from "../../extensions.js"; import { getCharaFilename, delay } from "./utils.js";
import { getCharaFilename, delay } from "../../utils.js"; import { power_user } from "./power-user.js";
import { power_user } from "../../power-user.js";
import { metadataKeys } from "./util.js";
// Keep track of where your extension is located, name should match repo name const extensionName = 'cfg';
const extensionName = "cfg";
const extensionFolderPath = `scripts/extensions/${extensionName}`;
const defaultSettings = { const defaultSettings = {
global: { global: {
"guidance_scale": 1, "guidance_scale": 1,
@ -199,7 +197,7 @@ function loadSettings() {
if (!promptSeparator.startsWith(`"`)) { if (!promptSeparator.startsWith(`"`)) {
promptSeparatorDisplay.unshift(`"`); promptSeparatorDisplay.unshift(`"`);
} }
if (!promptSeparator.endsWith(`"`)) { if (!promptSeparator.endsWith(`"`)) {
promptSeparatorDisplay.push(`"`); promptSeparatorDisplay.push(`"`);
} }
@ -279,14 +277,8 @@ function migrateSettings() {
} }
// This function is called when the extension is loaded // This function is called when the extension is loaded
jQuery(async () => { export function initCfg() {
// This is an example of loading HTML from a file $('#CFGClose').on('click', function () {
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
// Append settingsHtml to extensions_settings
// extension_settings and extensions_settings2 are the left and right columns of the settings menu
// Left should be extensions that deal with system functions and right should be visual/UI related
windowHtml.find('#CFGClose').on('click', function () {
$("#cfgConfig").transition({ $("#cfgConfig").transition({
opacity: 0, opacity: 0,
duration: 200, duration: 200,
@ -295,7 +287,7 @@ jQuery(async () => {
setTimeout(function () { $('#cfgConfig').hide() }, 200); setTimeout(function () { $('#cfgConfig').hide() }, 200);
}); });
windowHtml.find('#chat_cfg_guidance_scale').on('input', function() { $('#chat_cfg_guidance_scale').on('input', function() {
const numberValue = Number($(this).val()); const numberValue = Number($(this).val());
const success = setChatCfg(numberValue, settingType.guidance_scale); const success = setChatCfg(numberValue, settingType.guidance_scale);
if (success) { if (success) {
@ -303,15 +295,15 @@ jQuery(async () => {
} }
}); });
windowHtml.find('#chat_cfg_negative_prompt').on('input', function() { $('#chat_cfg_negative_prompt').on('input', function() {
setChatCfg($(this).val(), settingType.negative_prompt); setChatCfg($(this).val(), settingType.negative_prompt);
}); });
windowHtml.find('#chat_cfg_positive_prompt').on('input', function() { $('#chat_cfg_positive_prompt').on('input', function() {
setChatCfg($(this).val(), settingType.positive_prompt); setChatCfg($(this).val(), settingType.positive_prompt);
}); });
windowHtml.find('#chara_cfg_guidance_scale').on('input', function() { $('#chara_cfg_guidance_scale').on('input', function() {
const value = $(this).val(); const value = $(this).val();
const success = setCharCfg(value, settingType.guidance_scale); const success = setCharCfg(value, settingType.guidance_scale);
if (success) { if (success) {
@ -319,34 +311,34 @@ jQuery(async () => {
} }
}); });
windowHtml.find('#chara_cfg_negative_prompt').on('input', function() { $('#chara_cfg_negative_prompt').on('input', function() {
setCharCfg($(this).val(), settingType.negative_prompt); setCharCfg($(this).val(), settingType.negative_prompt);
}); });
windowHtml.find('#chara_cfg_positive_prompt').on('input', function() { $('#chara_cfg_positive_prompt').on('input', function() {
setCharCfg($(this).val(), settingType.positive_prompt); setCharCfg($(this).val(), settingType.positive_prompt);
}); });
windowHtml.find('#global_cfg_guidance_scale').on('input', function() { $('#global_cfg_guidance_scale').on('input', function() {
extension_settings.cfg.global.guidance_scale = Number($(this).val()); extension_settings.cfg.global.guidance_scale = Number($(this).val());
$('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2)); $('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2));
saveSettingsDebounced(); saveSettingsDebounced();
}); });
windowHtml.find('#global_cfg_negative_prompt').on('input', function() { $('#global_cfg_negative_prompt').on('input', function() {
extension_settings.cfg.global.negative_prompt = $(this).val(); extension_settings.cfg.global.negative_prompt = $(this).val();
saveSettingsDebounced(); saveSettingsDebounced();
}); });
windowHtml.find('#global_cfg_positive_prompt').on('input', function() { $('#global_cfg_positive_prompt').on('input', function() {
extension_settings.cfg.global.positive_prompt = $(this).val(); extension_settings.cfg.global.positive_prompt = $(this).val();
saveSettingsDebounced(); saveSettingsDebounced();
}); });
windowHtml.find(`input[name="cfg_prompt_combine"]`).on('input', function() { $(`input[name="cfg_prompt_combine"]`).on('input', function() {
const values = windowHtml.find(`input[name="cfg_prompt_combine"]`) const values = $('#cfgConfig').find(`input[name="cfg_prompt_combine"]`)
.filter(":checked") .filter(":checked")
.map(function() { return parseInt($(this).val()) }) .map(function() { return Number($(this).val()) })
.get() .get()
.filter((e) => !Number.isNaN(e)) || []; .filter((e) => !Number.isNaN(e)) || [];
@ -354,17 +346,17 @@ jQuery(async () => {
saveMetadataDebounced(); saveMetadataDebounced();
}); });
windowHtml.find(`#cfg_prompt_insertion_depth`).on('input', function() { $(`#cfg_prompt_insertion_depth`).on('input', function() {
chat_metadata[metadataKeys.prompt_insertion_depth] = Number($(this).val()); chat_metadata[metadataKeys.prompt_insertion_depth] = Number($(this).val());
saveMetadataDebounced(); saveMetadataDebounced();
}); });
windowHtml.find(`#cfg_prompt_separator`).on('input', function() { $(`#cfg_prompt_separator`).on('input', function() {
chat_metadata[metadataKeys.prompt_separator] = $(this).val(); chat_metadata[metadataKeys.prompt_separator] = $(this).val();
saveMetadataDebounced(); saveMetadataDebounced();
}); });
windowHtml.find('#groupchat_cfg_use_chara').on('input', function() { $('#groupchat_cfg_use_chara').on('input', function() {
const checked = !!$(this).prop('checked'); const checked = !!$(this).prop('checked');
chat_metadata[metadataKeys.groupchat_individual_chars] = checked chat_metadata[metadataKeys.groupchat_individual_chars] = checked
@ -375,20 +367,126 @@ jQuery(async () => {
saveMetadataDebounced(); saveMetadataDebounced();
}); });
$("#movingDivs").append(windowHtml);
initialLoadSettings(); initialLoadSettings();
if (extension_settings.cfg) { if (extension_settings.cfg) {
migrateSettings(); migrateSettings();
} }
const buttonHtml = $(await $.get(`${extensionFolderPath}/menuButton.html`)); $('#option_toggle_CFG').on('click', onCfgMenuItemClick);
buttonHtml.on('click', onCfgMenuItemClick)
buttonHtml.appendTo("#options_advanced");
// Hook events // Hook events
eventSource.on(event_types.CHAT_CHANGED, async () => { eventSource.on(event_types.CHAT_CHANGED, async () => {
await onChatChanged(); await onChatChanged();
}); });
}); }
export const cfgType = {
chat: 0,
chara: 1,
global: 2
}
export const metadataKeys = {
guidance_scale: "cfg_guidance_scale",
negative_prompt: "cfg_negative_prompt",
positive_prompt: "cfg_positive_prompt",
prompt_combine: "cfg_prompt_combine",
groupchat_individual_chars: "cfg_groupchat_individual_chars",
prompt_insertion_depth: "cfg_prompt_insertion_depth",
prompt_separator: "cfg_prompt_separator"
}
// Gets the CFG guidance scale
// If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
export function getGuidanceScale() {
if (!extension_settings.cfg) {
console.warn("CFG extension is not enabled. Skipping CFG guidance.");
return;
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) {
return {
type: cfgType.chat,
value: chatGuidanceScale
};
}
if ((!selected_group && charaCfg || groupchatCharOverride) && charaCfg?.guidance_scale !== 1) {
return {
type: cfgType.chara,
value: charaCfg.guidance_scale
};
}
if (extension_settings.cfg.global && extension_settings.cfg.global?.guidance_scale !== 1) {
return {
type: cfgType.global,
value: extension_settings.cfg.global.guidance_scale
};
}
}
/**
* Gets the CFG prompt separator.
* @returns {string} The CFG prompt separator
*/
function getCustomSeparator() {
const defaultSeparator = "\n";
try {
if (chat_metadata[metadataKeys.prompt_separator]) {
return JSON.parse(chat_metadata[metadataKeys.prompt_separator]);
}
return defaultSeparator;
} catch {
console.warn("Invalid JSON detected for prompt separator. Using default separator.");
return defaultSeparator;
}
}
// Gets the CFG prompt
export function getCfgPrompt(guidanceScale, isNegative) {
let splitCfgPrompt = [];
const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? [];
if (guidanceScale.type === cfgType.chat || cfgPromptCombine.includes(cfgType.chat)) {
splitCfgPrompt.unshift(
substituteParams(
chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt]
)
);
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
if (guidanceScale.type === cfgType.chara || cfgPromptCombine.includes(cfgType.chara)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt
)
);
}
if (guidanceScale.type === cfgType.global || cfgPromptCombine.includes(cfgType.global)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt
)
);
}
const customSeparator = getCustomSeparator();
const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator);
const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1;
console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
return {
value: combinedCfgPrompt,
depth: insertionDepth
};
}

View File

@ -11,7 +11,7 @@ export {
ModuleWorkerWrapper, ModuleWorkerWrapper,
}; };
let extensionNames = []; export let extensionNames = [];
let manifests = {}; let manifests = {};
const defaultUrl = "http://localhost:5100"; const defaultUrl = "http://localhost:5100";
@ -123,6 +123,7 @@ const extension_settings = {
apiUrl: defaultUrl, apiUrl: defaultUrl,
apiKey: '', apiKey: '',
autoConnect: false, autoConnect: false,
notifyUpdates: false,
disabledExtensions: [], disabledExtensions: [],
expressionOverrides: [], expressionOverrides: [],
memory: {}, memory: {},
@ -367,6 +368,15 @@ function addExtensionsButtonAndMenu() {
}); });
} }
function notifyUpdatesInputHandler() {
extension_settings.notifyUpdates = !!$('#extensions_notify_updates').prop('checked');
saveSettingsDebounced();
if (extension_settings.notifyUpdates) {
checkForExtensionUpdates(true);
}
}
/* $(document).on('click', function (e) { /* $(document).on('click', function (e) {
const target = $(e.target); const target = $(e.target);
if (target.is(dropdown)) return; if (target.is(dropdown)) return;
@ -582,16 +592,25 @@ async function showExtensionsDetails() {
let htmlExternal = '<h3>External Extensions:</h3>'; let htmlExternal = '<h3>External Extensions:</h3>';
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
const promises = [];
for (const extension of extensions) { for (const extension of extensions) {
const { isExternal, extensionHtml } = await getExtensionData(extension); promises.push(getExtensionData(extension));
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
} }
const settledPromises = await Promise.allSettled(promises);
settledPromises.forEach(promise => {
if (promise.status === 'fulfilled') {
const { isExternal, extensionHtml } = promise.value;
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
}
});
const html = ` const html = `
${getModuleInformation()} ${getModuleInformation()}
${htmlDefault} ${htmlDefault}
@ -609,6 +628,15 @@ async function showExtensionsDetails() {
*/ */
async function onUpdateClick() { async function onUpdateClick() {
const extensionName = $(this).data('name'); const extensionName = $(this).data('name');
await updateExtension(extensionName, false);
}
/**
* Updates a third-party extension via the API.
* @param {string} extensionName Extension folder name
* @param {boolean} quiet If true, don't show a success message
*/
async function updateExtension(extensionName, quiet) {
try { try {
const response = await fetch('/api/extensions/update', { const response = await fetch('/api/extensions/update', {
method: 'POST', method: 'POST',
@ -618,15 +646,20 @@ async function onUpdateClick() {
const data = await response.json(); const data = await response.json();
if (data.isUpToDate) { if (data.isUpToDate) {
toastr.success('Extension is already up to date'); if (!quiet) {
toastr.success('Extension is already up to date');
}
} else { } else {
toastr.success(`Extension updated to ${data.shortCommitHash}`); toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`);
}
if (!quiet) {
showExtensionsDetails();
} }
showExtensionsDetails();
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
} }
}; }
/** /**
* Handles the click event for the delete button of an extension. * Handles the click event for the delete button of an extension.
@ -639,23 +672,26 @@ async function onDeleteClick() {
// use callPopup to create a popup for the user to confirm before delete // use callPopup to create a popup for the user to confirm before delete
const confirmation = await callPopup(`Are you sure you want to delete ${extensionName}?`, 'delete_extension'); const confirmation = await callPopup(`Are you sure you want to delete ${extensionName}?`, 'delete_extension');
if (confirmation) { if (confirmation) {
try { await deleteExtension(extensionName);
const response = await fetch('/api/extensions/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ extensionName })
});
} catch (error) {
console.error('Error:', error);
}
toastr.success(`Extension ${extensionName} deleted`);
showExtensionsDetails();
// reload the page to remove the extension from the list
location.reload();
} }
}; };
export async function deleteExtension(extensionName) {
try {
const response = await fetch('/api/extensions/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ extensionName })
});
} catch (error) {
console.error('Error:', error);
}
toastr.success(`Extension ${extensionName} deleted`);
showExtensionsDetails();
// reload the page to remove the extension from the list
location.reload();
}
/** /**
* Fetches the version details of a specific extension. * Fetches the version details of a specific extension.
@ -680,9 +716,41 @@ async function getExtensionVersion(extensionName) {
} }
} }
/**
* Installs a third-party extension via the API.
* @param {string} url Extension repository URL
* @returns {Promise<void>}
*/
export async function installExtension(url) {
console.debug('Extension installation started', url);
toastr.info('Please wait...', 'Installing extension');
async function loadExtensionSettings(settings) { const request = await fetch('/api/extensions/install', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
if (!request.ok) {
toastr.info(request.statusText, 'Extension installation failed');
console.error('Extension installation failed', request.status, request.statusText);
return;
}
const response = await request.json();
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful');
console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
await loadExtensionSettings({}, false);
eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
}
/**
* Loads extension settings from the app settings.
* @param {object} settings App Settings
* @param {boolean} versionChanged Is this a version change?
*/
async function loadExtensionSettings(settings, versionChanged) {
if (settings.extension_settings) { if (settings.extension_settings) {
Object.assign(extension_settings, settings.extension_settings); Object.assign(extension_settings, settings.extension_settings);
} }
@ -690,15 +758,80 @@ async function loadExtensionSettings(settings) {
$("#extensions_url").val(extension_settings.apiUrl); $("#extensions_url").val(extension_settings.apiUrl);
$("#extensions_api_key").val(extension_settings.apiKey); $("#extensions_api_key").val(extension_settings.apiKey);
$("#extensions_autoconnect").prop('checked', extension_settings.autoConnect); $("#extensions_autoconnect").prop('checked', extension_settings.autoConnect);
$("#extensions_notify_updates").prop('checked', extension_settings.notifyUpdates);
// Activate offline extensions // Activate offline extensions
eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD); eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD);
extensionNames = await discoverExtensions(); extensionNames = await discoverExtensions();
manifests = await getManifests(extensionNames) manifests = await getManifests(extensionNames)
if (versionChanged) {
await autoUpdateExtensions();
}
await activateExtensions(); await activateExtensions();
if (extension_settings.autoConnect && extension_settings.apiUrl) { if (extension_settings.autoConnect && extension_settings.apiUrl) {
connectToApi(extension_settings.apiUrl); connectToApi(extension_settings.apiUrl);
} }
if (extension_settings.notifyUpdates) {
checkForExtensionUpdates(false);
}
}
/**
* Checks if there are updates available for 3rd-party extensions.
* @param {boolean} force Skip nag check
* @returns {Promise<any>}
*/
async function checkForExtensionUpdates(force) {
if (!force) {
const STORAGE_NAG_KEY = 'extension_update_nag';
const currentDate = new Date().toDateString();
// Don't nag more than once a day
if (localStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
return;
}
localStorage.setItem(STORAGE_NAG_KEY, currentDate);
}
const updatesAvailable = [];
const promises = [];
for (const [id, manifest] of Object.entries(manifests)) {
if (manifest.auto_update && id.startsWith('third-party')) {
const promise = new Promise(async (resolve, reject) => {
try {
const data = await getExtensionVersion(id.replace('third-party', ''));
if (data.isUpToDate === false) {
updatesAvailable.push(manifest.display_name);
}
resolve();
} catch (error) {
console.error('Error checking for extension updates', error);
reject();
}
});
promises.push(promise);
}
}
await Promise.allSettled(promises);
if (updatesAvailable.length > 0) {
toastr.info(`${updatesAvailable.map(x => `${x}`).join('\n')}`, 'Extension updates available');
}
}
async function autoUpdateExtensions() {
for (const [id, manifest] of Object.entries(manifests)) {
if (manifest.auto_update && id.startsWith('third-party')) {
console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`);
await updateExtension(id.replace('third-party', ''), true);
}
}
} }
async function runGenerationInterceptors(chat, contextSize) { async function runGenerationInterceptors(chat, contextSize) {
@ -721,8 +854,36 @@ jQuery(function () {
$("#extensions_connect").on('click', connectClickHandler); $("#extensions_connect").on('click', connectClickHandler);
$("#extensions_autoconnect").on('input', autoConnectInputHandler); $("#extensions_autoconnect").on('input', autoConnectInputHandler);
$("#extensions_details").on('click', showExtensionsDetails); $("#extensions_details").on('click', showExtensionsDetails);
$("#extensions_notify_updates").on('input', notifyUpdatesInputHandler);
$(document).on('click', '.toggle_disable', onDisableExtensionClick); $(document).on('click', '.toggle_disable', onDisableExtensionClick);
$(document).on('click', '.toggle_enable', onEnableExtensionClick); $(document).on('click', '.toggle_enable', onEnableExtensionClick);
$(document).on('click', '.btn_update', onUpdateClick); $(document).on('click', '.btn_update', onUpdateClick);
$(document).on('click', '.btn_delete', onDeleteClick); $(document).on('click', '.btn_delete', onDeleteClick);
/**
* Handles the click event for the third-party extension import button.
* Prompts the user to enter the Git URL of the extension to import.
* After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
* If the extension is imported successfully, a success message is displayed.
* If the extension import fails, an error message is displayed and the error is logged to the console.
* After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
*
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', async () => {
const html = `<h3>Enter the Git URL of the extension to install</h3>
<br>
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
<br>
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Extension install cancelled');
return;
}
const url = input.trim();
await installExtension(url);
});
}); });

View File

@ -0,0 +1,9 @@
<div class="m-b-1">
Are you sure you want to connect to '{{url}}'?
</div>
<div class="flex-container justifyCenter">
<label class="checkbox_label" for="assets-remember">
<input type="checkbox" id="assets-remember">
Don't ask again for this URL
</label>
</div>

View File

@ -1,14 +1,16 @@
/* /*
TODO: TODO:
- Check failed install file (0kb size ?)
*/ */
//const DEBUG_TONY_SAMA_FORK_MODE = false //const DEBUG_TONY_SAMA_FORK_MODE = true
import { getRequestHeaders, callPopup } from "../../../script.js"; import { getRequestHeaders, callPopup } from "../../../script.js";
import { deleteExtension, extensionNames, installExtension, renderExtensionTemplate } from "../../extensions.js";
import { getStringHash, isValidUrl } from "../../utils.js";
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'Assets'; const MODULE_NAME = 'assets';
const DEBUG_PREFIX = "<Assets module> "; const DEBUG_PREFIX = "<Assets module> ";
let previewAudio = null;
let ASSETS_JSON_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json" let ASSETS_JSON_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json"
const extensionName = "assets"; const extensionName = "assets";
@ -29,7 +31,7 @@ const defaultSettings = {
function downloadAssetsList(url) { function downloadAssetsList(url) {
updateCurrentAssets().then(function () { updateCurrentAssets().then(function () {
fetch(url) fetch(url, { cache: "no-cache" })
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
@ -47,8 +49,10 @@ function downloadAssetsList(url) {
} }
console.debug(DEBUG_PREFIX, "Updated available assets to", availableAssets); console.debug(DEBUG_PREFIX, "Updated available assets to", availableAssets);
// First extensions, then everything else
const assetTypes = Object.keys(availableAssets).sort((a, b) => (a === 'extension') ? -1 : (b === 'extension') ? 1 : 0);
for (const assetType in availableAssets) { for (const assetType of assetTypes) {
let assetTypeMenu = $('<div />', { id: "assets_audio_ambient_div", class: "assets-list-div" }); let assetTypeMenu = $('<div />', { id: "assets_audio_ambient_div", class: "assets-list-div" });
assetTypeMenu.append(`<h3>${assetType}</h3>`) assetTypeMenu.append(`<h3>${assetType}</h3>`)
for (const i in availableAssets[assetType]) { for (const i in availableAssets[assetType]) {
@ -59,7 +63,7 @@ function downloadAssetsList(url) {
element.append(label); element.append(label);
//if (DEBUG_TONY_SAMA_FORK_MODE) //if (DEBUG_TONY_SAMA_FORK_MODE)
// assetUrl = assetUrl.replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG // asset["url"] = asset["url"].replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG
console.debug(DEBUG_PREFIX, "Checking asset", asset["id"], asset["url"]); console.debug(DEBUG_PREFIX, "Checking asset", asset["id"], asset["url"]);
@ -71,18 +75,18 @@ function downloadAssetsList(url) {
label.addClass("fa-check"); label.addClass("fa-check");
this.classList.remove('asset-download-button-loading'); this.classList.remove('asset-download-button-loading');
element.on("click", assetDelete); element.on("click", assetDelete);
element.on("mouseenter", function(){ element.on("mouseenter", function () {
label.removeClass("fa-check"); label.removeClass("fa-check");
label.addClass("fa-trash"); label.addClass("fa-trash");
label.addClass("redOverlayGlow"); label.addClass("redOverlayGlow");
}).on("mouseleave", function(){ }).on("mouseleave", function () {
label.addClass("fa-check"); label.addClass("fa-check");
label.removeClass("fa-trash"); label.removeClass("fa-trash");
label.removeClass("redOverlayGlow"); label.removeClass("redOverlayGlow");
}); });
}; };
const assetDelete = async function() { const assetDelete = async function () {
element.off("click"); element.off("click");
await deleteAsset(assetType, asset["id"]); await deleteAsset(assetType, asset["id"]);
label.removeClass("fa-check"); label.removeClass("fa-check");
@ -98,11 +102,11 @@ function downloadAssetsList(url) {
label.toggleClass("fa-download"); label.toggleClass("fa-download");
label.toggleClass("fa-check"); label.toggleClass("fa-check");
element.on("click", assetDelete); element.on("click", assetDelete);
element.on("mouseenter", function(){ element.on("mouseenter", function () {
label.removeClass("fa-check"); label.removeClass("fa-check");
label.addClass("fa-trash"); label.addClass("fa-trash");
label.addClass("redOverlayGlow"); label.addClass("redOverlayGlow");
}).on("mouseleave", function(){ }).on("mouseleave", function () {
label.addClass("fa-check"); label.addClass("fa-check");
label.removeClass("fa-trash"); label.removeClass("fa-trash");
label.removeClass("redOverlayGlow"); label.removeClass("redOverlayGlow");
@ -114,14 +118,28 @@ function downloadAssetsList(url) {
element.on("click", assetInstall); element.on("click", assetInstall);
} }
console.debug(DEBUG_PREFIX, "Created element for BGM", asset["id"]) console.debug(DEBUG_PREFIX, "Created element for ", asset["id"])
const displayName = DOMPurify.sanitize(asset["name"] || asset["id"]);
const description = DOMPurify.sanitize(asset["description"] || "");
const url = isValidUrl(asset["url"]) ? asset["url"] : "";
const previewIcon = assetType == 'extension' ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
$(`<i></i>`) $(`<i></i>`)
.append(element) .append(element)
.append(`<span>${asset["id"]}</span>`) .append(`<div class="flex-container flexFlowColumn">
<span class="flex-container alignitemscenter">
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="Preview in browser">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
</span>
<span>${description}</span>
</div>`)
.appendTo(assetTypeMenu); .appendTo(assetTypeMenu);
} }
assetTypeMenu.appendTo("#assets_menu"); assetTypeMenu.appendTo("#assets_menu");
assetTypeMenu.on('click', 'a.asset_preview', previewAsset);
} }
$("#assets_menu").show(); $("#assets_menu").show();
@ -135,8 +153,37 @@ function downloadAssetsList(url) {
}); });
} }
function previewAsset(e) {
const href = $(this).attr('href');
const audioExtensions = ['.mp3', '.ogg', '.wav'];
if (audioExtensions.some(ext => href.endsWith(ext))) {
e.preventDefault();
if (previewAudio) {
previewAudio.pause();
if (previewAudio.src === href) {
previewAudio = null;
return;
}
}
previewAudio = new Audio(href);
previewAudio.play();
return;
}
}
function isAssetInstalled(assetType, filename) { function isAssetInstalled(assetType, filename) {
for (const i of currentAssets[assetType]) { let assetList = currentAssets[assetType];
if (assetType == 'extension') {
const thirdPartyMarker = "third-party/";
assetList = extensionNames.filter(x => x.startsWith(thirdPartyMarker)).map(x => x.replace(thirdPartyMarker, ''));
}
for (const i of assetList) {
//console.debug(DEBUG_PREFIX,i,filename) //console.debug(DEBUG_PREFIX,i,filename)
if (i.includes(filename)) if (i.includes(filename))
return true; return true;
@ -149,6 +196,13 @@ async function installAsset(url, assetType, filename) {
console.debug(DEBUG_PREFIX, "Downloading ", url); console.debug(DEBUG_PREFIX, "Downloading ", url);
const category = assetType; const category = assetType;
try { try {
if (category === 'extension') {
console.debug(DEBUG_PREFIX, "Installing extension ", url)
await installExtension(url);
console.debug(DEBUG_PREFIX, "Extension installed.")
return;
}
const body = { url, category, filename }; const body = { url, category, filename };
const result = await fetch('/api/assets/download', { const result = await fetch('/api/assets/download', {
method: 'POST', method: 'POST',
@ -170,6 +224,12 @@ async function deleteAsset(assetType, filename) {
console.debug(DEBUG_PREFIX, "Deleting ", assetType, filename); console.debug(DEBUG_PREFIX, "Deleting ", assetType, filename);
const category = assetType; const category = assetType;
try { try {
if (category === 'extension') {
console.debug(DEBUG_PREFIX, "Deleting extension ", filename)
await deleteExtension(filename);
console.debug(DEBUG_PREFIX, "Extension deleted.")
}
const body = { category, filename }; const body = { category, filename };
const result = await fetch('/api/assets/delete', { const result = await fetch('/api/assets/delete', {
method: 'POST', method: 'POST',
@ -214,24 +274,35 @@ async function updateCurrentAssets() {
// This function is called when the extension is loaded // This function is called when the extension is loaded
jQuery(async () => { jQuery(async () => {
// This is an example of loading HTML from a file // This is an example of loading HTML from a file
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`)); const windowHtml = $(renderExtensionTemplate(MODULE_NAME, 'window', {}));
const assetsJsonUrl = windowHtml.find('#assets-json-url-field'); const assetsJsonUrl = windowHtml.find('#assets-json-url-field');
assetsJsonUrl.val(ASSETS_JSON_URL); assetsJsonUrl.val(ASSETS_JSON_URL);
const connectButton = windowHtml.find('#assets-connect-button'); const connectButton = windowHtml.find('#assets-connect-button');
connectButton.on("click", async function () { connectButton.on("click", async function () {
const confirmation = await callPopup(`Are you sure you want to connect to '${assetsJsonUrl.val()}'?`, 'confirm') const url = String(assetsJsonUrl.val());
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
const skipConfirm = localStorage.getItem(rememberKey) === 'true';
const template = renderExtensionTemplate(MODULE_NAME, 'confirm', { url });
const confirmation = skipConfirm || await callPopup(template, 'confirm');
if (confirmation) { if (confirmation) {
try { try {
if (!skipConfirm) {
const rememberValue = Boolean($('#assets-remember').prop('checked'));
localStorage.setItem(rememberKey, String(rememberValue));
}
console.debug(DEBUG_PREFIX, "Confimation, loading assets..."); console.debug(DEBUG_PREFIX, "Confimation, loading assets...");
downloadAssetsList(assetsJsonUrl.val()); downloadAssetsList(url);
connectButton.removeClass("fa-plug-circle-exclamation"); connectButton.removeClass("fa-plug-circle-exclamation");
connectButton.removeClass("redOverlayGlow"); connectButton.removeClass("redOverlayGlow");
connectButton.addClass("fa-plug-circle-check"); connectButton.addClass("fa-plug-circle-check");
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
toastr.error(`Cannot get assets list from ${assetsJsonUrl.val()}`); toastr.error(`Cannot get assets list from ${url}`);
connectButton.removeClass("fa-plug-circle-check"); connectButton.removeClass("fa-plug-circle-check");
connectButton.addClass("fa-plug-circle-exclamation"); connectButton.addClass("fa-plug-circle-exclamation");
connectButton.removeClass("redOverlayGlow"); connectButton.removeClass("redOverlayGlow");

View File

@ -13,18 +13,31 @@
padding: 5px; padding: 5px;
} }
.assets-list-div h3 {
text-transform: capitalize;
}
.assets-list-div a {
color: inherit;
}
.assets-list-div i { .assets-list-div i {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: left; justify-content: left;
padding: 5px; padding: 5px;
font-style: normal;
} }
.assets-list-div i span{ .assets-list-div i span {
margin-left: 10px; margin-left: 10px;
} }
.assets-list-div i span:first-of-type {
font-weight: bold;
}
.asset-download-button { .asset-download-button {
position: relative; position: relative;
width: 50px; width: 50px;
@ -33,8 +46,8 @@
outline: none; outline: none;
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
} }
.asset-download-button:active { .asset-download-button:active {
background: #007a63; background: #007a63;
} }
@ -67,13 +80,11 @@
} }
@keyframes asset-download-button-loading-spinner { @keyframes asset-download-button-loading-spinner {
from { from {
transform: rotate(0turn); transform: rotate(0turn);
} }
to { to {
transform: rotate(1turn); transform: rotate(1turn);
}
} }
}

View File

@ -1,7 +1,7 @@
<div id="assets_ui"> <div id="assets_ui">
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<b>Assets</b> <b>Download Extensions & Assets</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">

View File

@ -1,917 +0,0 @@
/*
Ideas:
- Clean design of new ui
- change select text versus options for playing: audio
- cross fading between bgm / start a different time
- fading should appear before end when switching randomly
- Background based ambient sounds
- import option on background UI ?
- Allow background music edition using background menu
- https://fontawesome.com/icons/music?f=classic&s=solid
- https://codepen.io/noirsociety/pen/rNQxQwm
- https://codepen.io/xrocker/pen/abdKVGy
*/
import { saveSettingsDebounced, getRequestHeaders } from "../../../script.js";
import { getContext, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
import { isDataURL } from "../../utils.js";
export { MODULE_NAME };
const extensionName = "audio";
const extensionFolderPath = `scripts/extensions/${extensionName}`;
const MODULE_NAME = 'Audio';
const DEBUG_PREFIX = "<Audio module> ";
const UPDATE_INTERVAL = 1000;
const ASSETS_BGM_FOLDER = "bgm";
const ASSETS_AMBIENT_FOLDER = "ambient";
const CHARACTER_BGM_FOLDER = "bgm"
const FALLBACK_EXPRESSION = "neutral";
const DEFAULT_EXPRESSIONS = [
//"talkinghead",
"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"
];
const SPRITE_DOM_ID = "#expression-image";
let current_chat_id = null
let fallback_BGMS = null; // Initialized only once with module workers
let ambients = null; // Initialized only once with module workers
let characterMusics = {}; // Updated with module workers
let currentCharacterBGM = null;
let currentExpressionBGM = null;
let currentBackground = null;
let cooldownBGM = 0;
let bgmEnded = true;
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
enabled: false,
dynamic_bgm_enabled: false,
//dynamic_ambient_enabled: false,
bgm_locked: true,
bgm_muted: true,
bgm_volume: 50,
bgm_selected: null,
ambient_locked: true,
ambient_muted: true,
ambient_volume: 50,
ambient_selected: null,
bgm_cooldown: 30
}
function loadSettings() {
if (extension_settings.audio === undefined)
extension_settings.audio = {};
if (Object.keys(extension_settings.audio).length === 0) {
Object.assign(extension_settings.audio, defaultSettings)
}
$("#audio_enabled").prop('checked', extension_settings.audio.enabled);
$("#audio_dynamic_bgm_enabled").prop('checked', extension_settings.audio.dynamic_bgm_enabled);
//$("#audio_dynamic_ambient_enabled").prop('checked', extension_settings.audio.dynamic_ambient_enabled);
$("#audio_bgm_volume").text(extension_settings.audio.bgm_volume);
$("#audio_ambient_volume").text(extension_settings.audio.ambient_volume);
$("#audio_bgm_volume_slider").val(extension_settings.audio.bgm_volume);
$("#audio_ambient_volume_slider").val(extension_settings.audio.ambient_volume);
if (extension_settings.audio.bgm_muted) {
$("#audio_bgm_mute_icon").removeClass("fa-volume-high");
$("#audio_bgm_mute_icon").addClass("fa-volume-mute");
$("#audio_bgm_mute").addClass("redOverlayGlow");
$("#audio_bgm").prop("muted", true);
}
else {
$("#audio_bgm_mute_icon").addClass("fa-volume-high");
$("#audio_bgm_mute_icon").removeClass("fa-volume-mute");
$("#audio_bgm_mute").removeClass("redOverlayGlow");
$("#audio_bgm").prop("muted", false);
}
if (extension_settings.audio.bgm_locked) {
//$("#audio_bgm_lock_icon").removeClass("fa-lock-open");
//$("#audio_bgm_lock_icon").addClass("fa-lock");
$("#audio_bgm").attr("loop", true);
$("#audio_bgm_lock").addClass("redOverlayGlow");
}
else {
//$("#audio_bgm_lock_icon").removeClass("fa-lock");
//$("#audio_bgm_lock_icon").addClass("fa-lock-open");
$("#audio_bgm").attr("loop", false);
$("#audio_bgm_lock").removeClass("redOverlayGlow");
}
/*
if (extension_settings.audio.bgm_selected !== null) {
$("#audio_bgm_select").append(new Option(extension_settings.audio.bgm_selected, extension_settings.audio.bgm_selected));
$("#audio_bgm_select").val(extension_settings.audio.bgm_selected);
}*/
if (extension_settings.audio.ambient_locked) {
$("#audio_ambient_lock_icon").removeClass("fa-lock-open");
$("#audio_ambient_lock_icon").addClass("fa-lock");
$("#audio_ambient_lock").addClass("redOverlayGlow");
}
else {
$("#audio_ambient_lock_icon").removeClass("fa-lock");
$("#audio_ambient_lock_icon").addClass("fa-lock-open");
}
/*
if (extension_settings.audio.ambient_selected !== null) {
$("#audio_ambient_select").append(new Option(extension_settings.audio.ambient_selected, extension_settings.audio.ambient_selected));
$("#audio_ambient_select").val(extension_settings.audio.ambient_selected);
}*/
if (extension_settings.audio.ambient_muted) {
$("#audio_ambient_mute_icon").removeClass("fa-volume-high");
$("#audio_ambient_mute_icon").addClass("fa-volume-mute");
$("#audio_ambient_mute").addClass("redOverlayGlow");
$("#audio_ambient").prop("muted", true);
}
else {
$("#audio_ambient_mute_icon").addClass("fa-volume-high");
$("#audio_ambient_mute_icon").removeClass("fa-volume-mute");
$("#audio_ambient_mute").removeClass("redOverlayGlow");
$("#audio_ambient").prop("muted", false);
}
$("#audio_bgm_cooldown").val(extension_settings.audio.bgm_cooldown);
$("#audio_debug_div").hide(); // DBG: comment to see debug mode
}
async function onEnabledClick() {
extension_settings.audio.enabled = $('#audio_enabled').is(':checked');
if (extension_settings.audio.enabled) {
if ($("#audio_bgm").attr("src") != "")
$("#audio_bgm")[0].play();
if ($("#audio_ambient").attr("src") != "")
$("#audio_ambient")[0].play();
} else {
$("#audio_bgm")[0].pause();
$("#audio_ambient")[0].pause();
}
saveSettingsDebounced();
}
async function onDynamicBGMEnabledClick() {
extension_settings.audio.dynamic_bgm_enabled = $('#audio_dynamic_bgm_enabled').is(':checked');
currentCharacterBGM = null;
currentExpressionBGM = null;
cooldownBGM = 0;
saveSettingsDebounced();
}
/*
async function onDynamicAmbientEnabledClick() {
extension_settings.audio.dynamic_ambient_enabled = $('#audio_dynamic_ambient_enabled').is(':checked');
currentBackground = null;
saveSettingsDebounced();
}
*/
async function onBGMLockClick() {
extension_settings.audio.bgm_locked = !extension_settings.audio.bgm_locked;
if (extension_settings.audio.bgm_locked) {
extension_settings.audio.bgm_selected = $("#audio_bgm_select").val();
$("#audio_bgm").attr("loop", true);
}
else {
$("#audio_bgm").attr("loop", false);
}
//$("#audio_bgm_lock_icon").toggleClass("fa-lock");
//$("#audio_bgm_lock_icon").toggleClass("fa-lock-open");
$("#audio_bgm_lock").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onBGMRandomClick() {
var select = document.getElementById('audio_bgm_select');
var items = select.getElementsByTagName('option');
if (items.length < 2)
return;
var index;
do {
index = Math.floor(Math.random() * items.length);
} while (index == select.selectedIndex);
select.selectedIndex = index;
onBGMSelectChange();
}
async function onBGMMuteClick() {
extension_settings.audio.bgm_muted = !extension_settings.audio.bgm_muted;
$("#audio_bgm_mute_icon").toggleClass("fa-volume-high");
$("#audio_bgm_mute_icon").toggleClass("fa-volume-mute");
$("#audio_bgm").prop("muted", !$("#audio_bgm").prop("muted"));
$("#audio_bgm_mute").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onAmbientLockClick() {
extension_settings.audio.ambient_locked = !extension_settings.audio.ambient_locked;
if (extension_settings.audio.ambient_locked)
extension_settings.audio.ambient_selected = $("#audio_ambient_select").val();
else {
extension_settings.audio.ambient_selected = null;
currentBackground = null;
}
$("#audio_ambient_lock_icon").toggleClass("fa-lock");
$("#audio_ambient_lock_icon").toggleClass("fa-lock-open");
$("#audio_ambient_lock").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onAmbientMuteClick() {
extension_settings.audio.ambient_muted = !extension_settings.audio.ambient_muted;
$("#audio_ambient_mute_icon").toggleClass("fa-volume-high");
$("#audio_ambient_mute_icon").toggleClass("fa-volume-mute");
$("#audio_ambient").prop("muted", !$("#audio_ambient").prop("muted"));
$("#audio_ambient_mute").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onBGMVolumeChange() {
extension_settings.audio.bgm_volume = ~~($("#audio_bgm_volume_slider").val());
$("#audio_bgm").prop("volume", extension_settings.audio.bgm_volume * 0.01);
$("#audio_bgm_volume").text(extension_settings.audio.bgm_volume);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
}
async function onAmbientVolumeChange() {
extension_settings.audio.ambient_volume = ~~($("#audio_ambient_volume_slider").val());
$("#audio_ambient").prop("volume", extension_settings.audio.ambient_volume * 0.01);
$("#audio_ambient_volume").text(extension_settings.audio.ambient_volume);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED Ambient MAX TO",extension_settings.audio.ambient_volume);
}
async function onBGMSelectChange() {
extension_settings.audio.bgm_selected = $("#audio_bgm_select").val();
updateBGM(true);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
}
async function onAmbientSelectChange() {
extension_settings.audio.ambient_selected = $("#audio_ambient_select").val();
updateAmbient(true);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
}
async function onBGMCooldownInput() {
extension_settings.audio.bgm_cooldown = ~~($("#audio_bgm_cooldown").val());
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
saveSettingsDebounced();
console.debug(DEBUG_PREFIX, "UPDATED BGM cooldown to", extension_settings.audio.bgm_cooldown);
}
//#############################//
// API Calls //
//#############################//
async function getAssetsList(type) {
console.debug(DEBUG_PREFIX, "getting assets of type", type);
try {
const result = await fetch(`/api/assets/get`, {
method: 'POST',
headers: getRequestHeaders(),
});
const assets = result.ok ? (await result.json()) : { type: [] };
console.debug(DEBUG_PREFIX, "Found assets:", assets);
return assets[type];
}
catch (err) {
console.log(err);
return [];
}
}
async function getCharacterBgmList(name) {
console.debug(DEBUG_PREFIX, "getting bgm list for", name);
try {
const result = await fetch(`/api/assets/character?name=${encodeURIComponent(name)}&category=${CHARACTER_BGM_FOLDER}`, {
method: 'POST',
headers: getRequestHeaders(),
});
let musics = result.ok ? (await result.json()) : [];
return musics;
}
catch (err) {
console.log(err);
return [];
}
}
//#############################//
// Module Worker //
//#############################//
function fillBGMSelect() {
let found_last_selected_bgm = false;
// Update bgm list in UI
$("#audio_bgm_select")
.find('option')
.remove();
for (const file of fallback_BGMS) {
$('#audio_bgm_select').append(new Option("asset: " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
if (file === extension_settings.audio.bgm_selected) {
$('#audio_bgm_select').val(extension_settings.audio.bgm_selected);
found_last_selected_bgm = true;
}
}
// Update bgm list in UI
for (const char in characterMusics)
for (const e in characterMusics[char])
for (const file of characterMusics[char][e]) {
$('#audio_bgm_select').append(new Option(char + ": " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
if (file === extension_settings.audio.bgm_selected) {
$('#audio_bgm_select').val(extension_settings.audio.bgm_selected);
found_last_selected_bgm = true;
}
}
if (!found_last_selected_bgm) {
$('#audio_bgm_select').val($("#audio_bgm_select option:first").val());
extension_settings.audio.bgm_selected = null;
}
}
/*
- Update ambient sound
- Update character BGM
- Solo dynamique expression
- Group only neutral bgm
*/
async function moduleWorker() {
const moduleEnabled = extension_settings.audio.enabled;
if (moduleEnabled) {
if (cooldownBGM > 0)
cooldownBGM -= UPDATE_INTERVAL;
if (fallback_BGMS == null) {
console.debug(DEBUG_PREFIX, "Updating audio bgm assets...");
fallback_BGMS = await getAssetsList(ASSETS_BGM_FOLDER);
fallback_BGMS = fallback_BGMS.filter((filename) => filename != ".placeholder")
console.debug(DEBUG_PREFIX, "Detected assets:", fallback_BGMS);
fillBGMSelect();
}
if (ambients == null) {
console.debug(DEBUG_PREFIX, "Updating audio ambient assets...");
ambients = await getAssetsList(ASSETS_AMBIENT_FOLDER);
ambients = ambients.filter((filename) => filename != ".placeholder")
console.debug(DEBUG_PREFIX, "Detected assets:", ambients);
// Update bgm list in UI
$("#audio_ambient_select")
.find('option')
.remove();
if (extension_settings.audio.ambient_selected !== null) {
let ambient_label = extension_settings.audio.ambient_selected;
if (ambient_label.includes("assets"))
ambient_label = "asset: " + ambient_label.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, "");
else {
ambient_label = ambient_label.substring("/characters/".length);
ambient_label = ambient_label.substring(0, ambient_label.indexOf("/")) + ": " + ambient_label.substring(ambient_label.indexOf("/") + "/bgm/".length);
ambient_label = ambient_label.replace(/\.[^/.]+$/, "");
}
$('#audio_ambient_select').append(new Option(ambient_label, extension_settings.audio.ambient_selected));
}
for (const file of ambients) {
if (file !== extension_settings.audio.ambient_selected)
$("#audio_ambient_select").append(new Option("asset: " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
}
}
// 1) Update ambient audio
// ---------------------------
//if (extension_settings.audio.dynamic_ambient_enabled) {
let newBackground = $("#bg1").css("background-image");
const custom_background = getContext()["chatMetadata"]["custom_background"];
if (custom_background !== undefined)
newBackground = custom_background
if (!isDataURL(newBackground)) {
newBackground = newBackground.substring(newBackground.lastIndexOf("/") + 1).replace(/\.[^/.]+$/, "").replaceAll("%20", "-").replaceAll(" ", "-"); // remove path and spaces
//console.debug(DEBUG_PREFIX,"Current backgroung:",newBackground);
if (currentBackground !== newBackground) {
currentBackground = newBackground;
console.debug(DEBUG_PREFIX, "Changing ambient audio for", currentBackground);
updateAmbient();
}
}
//}
const context = getContext();
//console.debug(DEBUG_PREFIX,context);
if (context.chat.length == 0)
return;
let chatIsGroup = context.chat[0].is_group;
let newCharacter = null;
// 1) Update BGM (single chat)
// -----------------------------
if (!chatIsGroup) {
// Reset bgm list on new chat
if (context.chatId != current_chat_id) {
current_chat_id = context.chatId;
characterMusics = {};
cooldownBGM = 0;
}
newCharacter = context.name2;
//console.log(DEBUG_PREFIX,"SOLO CHAT MODE"); // DBG
// 1.1) First time loading chat
if (characterMusics[newCharacter] === undefined) {
await loadCharacterBGM(newCharacter);
currentExpressionBGM = FALLBACK_EXPRESSION;
//currentCharacterBGM = newCharacter;
//updateBGM();
//cooldownBGM = BGM_UPDATE_COOLDOWN;
return;
}
// 1.2) Switched chat
if (currentCharacterBGM !== newCharacter) {
currentCharacterBGM = newCharacter;
try {
await updateBGM(false, true);
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM character, will try again");
currentCharacterBGM = null
}
return;
}
const newExpression = getNewExpression();
// 1.3) Same character but different expression
if (currentExpressionBGM !== newExpression) {
// Check cooldown
if (cooldownBGM > 0) {
//console.debug(DEBUG_PREFIX,"(SOLO) BGM switch on cooldown:",cooldownBGM);
return;
}
try {
currentExpressionBGM = newExpression;
await updateBGM();
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
console.debug(DEBUG_PREFIX, "(SOLO) Updated current character expression to", currentExpressionBGM, "cooldown", cooldownBGM);
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM expression, will try again");
currentCharacterBGM = null
}
return;
}
return;
}
// 2) Update BGM (group chat)
// -----------------------------
// Load current chat character bgms
// Reset bgm list on new chat
if (context.chatId != current_chat_id) {
current_chat_id = context.chatId;
characterMusics = {};
cooldownBGM = 0;
for (const message of context.chat) {
if (characterMusics[message.name] === undefined)
await loadCharacterBGM(message.name);
}
try {
newCharacter = context.chat[context.chat.length - 1].name;
currentCharacterBGM = newCharacter;
await updateBGM(false, true);
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
currentCharacterBGM = newCharacter;
currentExpressionBGM = FALLBACK_EXPRESSION;
console.debug(DEBUG_PREFIX, "(GROUP) Updated current character BGM to", currentExpressionBGM, "cooldown", cooldownBGM);
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM group, will try again");
currentCharacterBGM = null
}
return;
}
newCharacter = context.chat[context.chat.length - 1].name;
const userName = context.name1;
if (newCharacter !== undefined && newCharacter != userName) {
//console.log(DEBUG_PREFIX,"GROUP CHAT MODE"); // DBG
// 2.1) New character appear
if (characterMusics[newCharacter] === undefined) {
await loadCharacterBGM(newCharacter);
return;
}
// 2.2) Switched char
if (currentCharacterBGM !== newCharacter) {
// Check cooldown
if (cooldownBGM > 0) {
console.debug(DEBUG_PREFIX, "(GROUP) BGM switch on cooldown:", cooldownBGM);
return;
}
try {
currentCharacterBGM = newCharacter;
await updateBGM();
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
currentCharacterBGM = newCharacter;
currentExpressionBGM = FALLBACK_EXPRESSION;
console.debug(DEBUG_PREFIX, "(GROUP) Updated current character BGM to", currentExpressionBGM, "cooldown", cooldownBGM);
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM group, will try again");
currentCharacterBGM = null
}
return;
}
/*
const newExpression = getNewExpression();
// 1.3) Same character but different expression
if (currentExpressionBGM !== newExpression) {
// Check cooldown
if (cooldownBGM > 0) {
console.debug(DEBUG_PREFIX,"BGM switch on cooldown:",cooldownBGM);
return;
}
cooldownBGM = BGM_UPDATE_COOLDOWN;
currentExpressionBGM = newExpression;
console.debug(DEBUG_PREFIX,"Updated current character expression to",currentExpressionBGM);
updateBGM();
return;
}
return;*/
}
// Case 3: Same character/expression or BGM switch on cooldown keep playing same BGM
//console.debug(DEBUG_PREFIX,"Nothing to do for",currentCharacterBGM, newCharacter, currentExpressionBGM, cooldownBGM);
}
}
async function loadCharacterBGM(newCharacter) {
console.debug(DEBUG_PREFIX, "New character detected, loading BGM folder of", newCharacter);
// 1.1) First time character appear, load its music folder
const audio_file_paths = await getCharacterBgmList(newCharacter);
//console.debug(DEBUG_PREFIX, "Recieved", audio_file_paths);
// Initialise expression/files mapping
characterMusics[newCharacter] = {};
for (const e of DEFAULT_EXPRESSIONS)
characterMusics[newCharacter][e] = [];
for (const i of audio_file_paths) {
//console.debug(DEBUG_PREFIX,"File found:",i);
for (const e of DEFAULT_EXPRESSIONS)
if (i.includes(e))
characterMusics[newCharacter][e].push(i);
}
console.debug(DEBUG_PREFIX, "Updated BGM map of", newCharacter, "to", characterMusics[newCharacter]);
fillBGMSelect();
}
function getNewExpression() {
let newExpression;
// HACK: use sprite file name as expression detection
if (!$(SPRITE_DOM_ID).length) {
console.error(DEBUG_PREFIX, "ERROR: expression sprite does not exist, cannot extract expression from ", SPRITE_DOM_ID)
return FALLBACK_EXPRESSION;
}
const spriteFile = $("#expression-image").attr("src");
newExpression = spriteFile.substring(spriteFile.lastIndexOf("/") + 1).replace(/\.[^/.]+$/, "");
//
// No sprite to detect expression
if (newExpression == "") {
//console.info(DEBUG_PREFIX,"Warning: no expression extracted from sprite, switch to",FALLBACK_EXPRESSION);
newExpression = FALLBACK_EXPRESSION;
}
if (!DEFAULT_EXPRESSIONS.includes(newExpression)) {
console.info(DEBUG_PREFIX, "Warning:", newExpression, " is not a handled expression, expected one of", FALLBACK_EXPRESSION);
return FALLBACK_EXPRESSION;
}
return newExpression;
}
async function updateBGM(isUserInput = false, newChat = false) {
if (!isUserInput && !extension_settings.audio.dynamic_bgm_enabled && $("#audio_bgm").attr("src") != "" && !bgmEnded && !newChat) {
console.debug(DEBUG_PREFIX, "BGM already playing and dynamic switch disabled, no update done");
return;
}
let audio_file_path = ""
if (isUserInput || (extension_settings.audio.bgm_locked && extension_settings.audio.bgm_selected !== null)) {
audio_file_path = extension_settings.audio.bgm_selected;
if (isUserInput)
console.debug(DEBUG_PREFIX, "User selected BGM", audio_file_path);
if (extension_settings.audio.bgm_locked)
console.debug(DEBUG_PREFIX, "BGM locked keeping current audio", audio_file_path);
}
else {
let audio_files = null;
if (extension_settings.audio.dynamic_bgm_enabled) {
extension_settings.audio.bgm_selected = null;
saveSettingsDebounced();
audio_files = characterMusics[currentCharacterBGM][currentExpressionBGM];// Try char expression BGM
if (audio_files === undefined || audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No BGM for", currentCharacterBGM, currentExpressionBGM);
audio_files = characterMusics[currentCharacterBGM][FALLBACK_EXPRESSION]; // Try char FALLBACK BGM
if (audio_files === undefined || audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No default BGM for", currentCharacterBGM, FALLBACK_EXPRESSION, "switch to ST BGM");
audio_files = fallback_BGMS; // ST FALLBACK BGM
if (audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No default BGM file found, bgm folder may be empty.");
return;
}
}
}
}
else {
audio_files = [];
$("#audio_bgm_select option").each(function () { audio_files.push($(this).val()); });
}
audio_file_path = audio_files[Math.floor(Math.random() * audio_files.length)];
}
console.log(DEBUG_PREFIX, "Updating BGM");
console.log(DEBUG_PREFIX, "Checking file", audio_file_path);
try {
const response = await fetch(audio_file_path);
if (!response.ok) {
console.log(DEBUG_PREFIX, "File not found!")
}
else {
console.log(DEBUG_PREFIX, "Switching BGM to", currentExpressionBGM);
$("#audio_bgm_select").val(audio_file_path);
const audio = $("#audio_bgm");
if (audio.attr("src") == audio_file_path && !bgmEnded) {
console.log(DEBUG_PREFIX, "Already playing, ignored");
return;
}
let fade_time = 2000;
bgmEnded = false;
if (isUserInput || extension_settings.audio.bgm_locked) {
audio.attr("src", audio_file_path);
audio[0].play();
}
else {
audio.animate({ volume: 0.0 }, fade_time, function () {
audio.attr("src", audio_file_path);
audio[0].play();
audio.volume = extension_settings.audio.bgm_volume * 0.01;
audio.animate({ volume: extension_settings.audio.bgm_volume * 0.01 }, fade_time);
});
}
}
} catch (error) {
console.log(DEBUG_PREFIX, "Error while trying to fetch", audio_file_path, ":", error);
}
}
async function updateAmbient(isUserInput = false) {
let audio_file_path = null;
if (isUserInput || extension_settings.audio.ambient_locked) {
audio_file_path = extension_settings.audio.ambient_selected;
if (isUserInput)
console.debug(DEBUG_PREFIX, "User selected Ambient", audio_file_path);
if (extension_settings.audio.bgm_locked)
console.debug(DEBUG_PREFIX, "Ambient locked keeping current audio", audio_file_path);
}
else {
extension_settings.audio.ambient_selected = null;
for (const i of ambients) {
console.debug(i)
if (i.includes(currentBackground)) {
audio_file_path = i;
break;
}
}
}
if (audio_file_path === null) {
console.debug(DEBUG_PREFIX, "No bgm file found for background", currentBackground);
const audio = $("#audio_ambient");
audio.attr("src", "");
audio[0].pause();
return;
}
//const audio_file_path = AMBIENT_FOLDER+currentBackground+".mp3";
console.log(DEBUG_PREFIX, "Updating ambient");
console.log(DEBUG_PREFIX, "Checking file", audio_file_path);
$("#audio_ambient_select").val(audio_file_path);
let fade_time = 2000;
if (isUserInput)
fade_time = 0;
const audio = $("#audio_ambient");
if (audio.attr("src") == audio_file_path) {
console.log(DEBUG_PREFIX, "Already playing, ignored");
return;
}
audio.animate({ volume: 0.0 }, fade_time, function () {
audio.attr("src", audio_file_path);
audio[0].play();
audio.volume = extension_settings.audio.ambient_volume * 0.01;
audio.animate({ volume: extension_settings.audio.ambient_volume * 0.01 }, fade_time);
});
}
/**
* Handles wheel events on volume sliders.
* @param {WheelEvent} e Event
*/
function onVolumeSliderWheelEvent(e) {
const slider = $(this);
e.preventDefault();
e.stopPropagation();
const delta = e.deltaY / 20;
const sliderVal = Number(slider.val());
let newVal = sliderVal - delta;
if (newVal < 0) {
newVal = 0;
} else if (newVal > 100) {
newVal = 100;
}
slider.val(newVal).trigger('input');
}
//#############################//
// Extension load //
//#############################//
// This function is called when the extension is loaded
jQuery(async () => {
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
$('#extensions_settings').append(windowHtml);
loadSettings();
$("#audio_enabled").on("click", onEnabledClick);
$("#audio_dynamic_bgm_enabled").on("click", onDynamicBGMEnabledClick);
//$("#audio_dynamic_ambient_enabled").on("click", onDynamicAmbientEnabledClick);
//$("#audio_bgm").attr("loop", false);
$("#audio_ambient").attr("loop", true);
$("#audio_bgm").hide();
$("#audio_bgm_lock").on("click", onBGMLockClick);
$("#audio_bgm_mute").on("click", onBGMMuteClick);
$("#audio_bgm_volume_slider").on("input", onBGMVolumeChange);
$("#audio_bgm_random").on("click", onBGMRandomClick);
$("#audio_ambient").hide();
$("#audio_ambient_lock").on("click", onAmbientLockClick);
$("#audio_ambient_mute").on("click", onAmbientMuteClick);
$("#audio_ambient_volume_slider").on("input", onAmbientVolumeChange);
document.getElementById('audio_ambient_volume_slider').addEventListener('wheel', onVolumeSliderWheelEvent, { passive: false });
document.getElementById('audio_bgm_volume_slider').addEventListener('wheel', onVolumeSliderWheelEvent, { passive: false });
$("#audio_bgm_cooldown").on("input", onBGMCooldownInput);
// Reset assets container, will be redected like if ST restarted
$("#audio_refresh_assets").on("click", function () {
console.debug(DEBUG_PREFIX, "Refreshing audio assets");
current_chat_id = null
fallback_BGMS = null;
ambients = null;
characterMusics = {};
currentCharacterBGM = null;
currentExpressionBGM = null;
currentBackground = null;
})
$("#audio_bgm_select").on("change", onBGMSelectChange);
$("#audio_ambient_select").on("change", onAmbientSelectChange);
// DBG
$("#audio_debug").on("click", function () {
if ($("#audio_debug").is(':checked')) {
$("#audio_bgm").show();
$("#audio_ambient").show();
}
else {
$("#audio_bgm").hide();
$("#audio_ambient").hide();
}
});
//
$("#audio_bgm").on("ended", function () {
console.debug(DEBUG_PREFIX, "END OF BGM")
if (!extension_settings.audio.bgm_locked) {
bgmEnded = true;
updateBGM();
}
});
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
moduleWorker();
});

View File

@ -1,11 +0,0 @@
{
"display_name": "Dynamic Audio",
"loading_order": 14,
"requires": [],
"optional": ["classify"],
"js": "index.js",
"css": "style.css",
"author": "Keij#6799 and Deffcolony",
"version": "0.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,92 +0,0 @@
.audio-ui-block {
margin-bottom: 1em;
}
.audio-mixer-div {
display: flex;
flex-direction: row;
padding: 5px;
background-color: rgba(38, 38, 38, 0.5);
border: 1px rgb(75, 75, 75) solid;
border-radius: 10px;
}
.audio-label {
display: block;
text-align: center;
}
.audio-volume-div {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.audio-lock-button {
width: 100%;
height: 2em;
}
.audio-random-button {
width: 100%;
height: 2em;
}
.audio-mute-button {
width: 100%;
height: 2em;
}
.audio-slider {
width: 100% !important;
vertical-align: center;
}
.audio-mute-button-muted {
color: red;
}
#audio_refresh_assets {
width: 50px;
height: 30px;
}
.audio-mixer-mute {
width: 10%;
}
.audio-mixer-lock {
width: 10%;
}
.audio-mixer-random {
width: 10%;
}
.audio-container {
display: flex;
gap: 10px;
align-items: center;
}
.audio-container>.vol {
width: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.audio-container>.vol>input {
width: 100%;
}
.audio-container>.playlist {
flex-grow: 1;
}
.audio-container>.playlist>select {
height: 100%;
margin: 0 !important;
}

View File

@ -1,103 +0,0 @@
<div id="audio_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Dynamic Audio</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>
<label class="checkbox_label" for="audio_enabled">
<input type="checkbox" id="audio_enabled" name="audio_enabled">
<small>Enabled</small>
</label>
<div id="audio_bgm_dynamic_enable_div">
<label class="checkbox_label" for="audio_dynamic_bgm_enabled">
<input type="checkbox" id="audio_dynamic_bgm_enabled" name="audio_dynamic_bgm_enabled">
<small>Enable expression BGM switch (req. character expression)</small>
</label>
</div>
<div id="audio_debug_div">
<label class="checkbox_label" for="audio_debug">
<input type="checkbox" id="audio_debug" name="audio_debug">
<small>Debug</small>
</label>
</div>
<div>
<label for="audio_refresh_assets">Refresh assets</label>
<div id="audio_refresh_assets" class="menu_button">
<i class="fa-solid fa-refresh fa-lg"></i>
</div>
</div>
</div>
<div>
<div class="audio-ui-block">
<label for="audio_bgm_volume_slider">Music</label>
<div class="audio-mixer-div audio-container">
<div class="audio-mixer-element audio-mixer-mute">
<div id="audio_bgm_mute" class="menu_button audio-mute-button">
<i class="fa-solid fa-volume-high fa-lg fa-fw" id="audio_bgm_mute_icon"></i>
</div>
</div>
<div class="audio-mixer-element vol audio-mixer-volume">
<input type="range" class ="audio-slider" id ="audio_bgm_volume_slider" value = "0" maxlength ="100">
</div>
<div class="audio-mixer-element playlist audio-mixer-playlist">
<select id="audio_bgm_select">
</select>
</div>
<div class="audio-mixer-element audio-mixer-lock">
<div id="audio_bgm_lock" class="menu_button audio-lock-button">
<i class="fa-solid fa-repeat fa-lg fa-fw" id="audio_bgm_lock_icon"></i>
</div>
</div>
<div class="audio-mixer-element audio-mixer-random">
<div id="audio_bgm_random" class="menu_button audio-random-button">
<i class="fa-solid fa-random fa-lg fa-fw" id="audio_bgm_random_icon"></i>
</div>
</div>
</div>
<audio id="audio_bgm" controls src="">
</div>
<div>
<label for="audio_ambient_volume_slider">Ambient</label>
<div class="audio-mixer-div audio-container">
<div class="audio-mixer-element audio-mixer-mute">
<div id="audio_ambient_mute" class="menu_button audio-mute-button">
<i class="fa-solid fa-volume-high fa-lg fa-fw" id="audio_ambient_mute_icon"></i>
</div>
</div>
<div class="audio-mixer-element vol audio-mixer-volume">
<input type="range" class ="audio-slider" id ="audio_ambient_volume_slider" value = "0" maxlength ="100">
</div>
<div class="audio-mixer-element playlist audio-mixer-playlist">
<select id="audio_ambient_select">
</select>
</div>
<div class="audio-mixer-element">
<div id="audio_ambient_lock" class="menu_button audio-lock-button">
<i class="fa-solid fa-lock-open fa-lg fa-fw" id="audio_ambient_lock_icon"></i>
</div>
</div>
</div>
<audio id="audio_ambient" controls src="">
</div>
<div>
<label for="audio_bgm_cooldown">Music update cooldown (in seconds)</label>
<input id="audio_bgm_cooldown" class="text_pole wide30p">
</div>
</div>
<div>
<b>Hint:</b>
<i>
Create new folder in the
<b>public/characters/</b>
folder and name it as the name of the character.
Create a folder name <b>bgm</b> inside of it.
Put bgm music with expressions there. File names should follow the pattern:
<it>[expression_label]_[number].mp3</it>
By default one of the <it>neutral_[number].mp3</it> will play if classify module is not active.
</i>
</div>
</div>
</div>
</div>

View File

@ -1,178 +0,0 @@
import { eventSource, event_types, generateQuietPrompt } from "../../../script.js";
import { getContext, saveMetadataDebounced } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { stringFormat } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'backgrounds';
const METADATA_KEY = 'custom_background';
/**
* @param {string} background
*/
function forceSetBackground(background) {
saveBackgroundMetadata(background);
setCustomBackground();
}
async function moduleWorker() {
if (hasCustomBackground()) {
$('#unlock_background').show();
$('#lock_background').hide();
setCustomBackground();
}
else {
$('#unlock_background').hide();
$('#lock_background').show();
unsetCustomBackground();
}
}
function onLockBackgroundClick() {
const bgImage = window.getComputedStyle(document.getElementById('bg1')).backgroundImage;
// Extract the URL from the CSS string
const urlRegex = /url\((['"])?(.*?)\1\)/;
const matches = bgImage.match(urlRegex);
const url = matches[2];
// Remove the protocol and host, leaving the relative URL
const relativeUrl = new URL(url).pathname;
const relativeBgImage = `url("${relativeUrl}")`
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
$('#unlock_background').show();
$('#lock_background').hide();
}
function onUnlockBackgroundClick() {
removeBackgroundMetadata();
unsetCustomBackground();
$('#unlock_background').hide();
$('#lock_background').show();
}
function hasCustomBackground() {
const context = getContext();
return !!context.chatMetadata[METADATA_KEY];
}
function saveBackgroundMetadata(file) {
const context = getContext();
context.chatMetadata[METADATA_KEY] = file;
saveMetadataDebounced();
}
function removeBackgroundMetadata() {
const context = getContext();
delete context.chatMetadata[METADATA_KEY];
saveMetadataDebounced();
}
function setCustomBackground() {
const context = getContext();
const file = context.chatMetadata[METADATA_KEY];
// bg already set
if (document.getElementById("bg_custom").style.backgroundImage == file) {
return;
}
$("#bg_custom").css("background-image", file);
$("#custom_bg_preview").css("background-image", file);
}
function unsetCustomBackground() {
$("#bg_custom").css("background-image", 'none');
$("#custom_bg_preview").css("background-image", 'none');
}
function onSelectBackgroundClick() {
const bgfile = $(this).attr("bgfile");
if (hasCustomBackground()) {
saveBackgroundMetadata(`url("backgrounds/${bgfile}")`);
setCustomBackground();
}
}
const autoBgPrompt = `Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}`;
async function autoBackgroundCommand() {
const options = Array.from(document.querySelectorAll('.BGSampleTitle')).map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return;
}
const list = options.map(option => `- ${option.text}`).join('\n');
const prompt = stringFormat(autoBgPrompt, list);
const reply = await generateQuietPrompt(prompt);
const fuse = new Fuse(options, { keys: ['text'] });
const bestMatch = fuse.search(reply, { limit: 1 });
if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.');
return;
}
console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click();
}
$(document).ready(function () {
function addSettings() {
const html = `
<div class="background_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Chat Backgrounds</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="background_controls">
<div id="lock_background" class="menu_button">
<i class="fa-solid fa-lock"></i>
Lock
</div>
<div id="unlock_background" class="menu_button">
<i class="fa-solid fa-unlock"></i>
Unlock
</div>
<small>
Press "Lock" to assign a currently selected background to a character or group chat.<br>
Any background image selected while lock is engaged will be saved automatically.
</small>
</div>
<div class="background_controls">
<div id="auto_background" class="menu_button">
<i class="fa-solid fa-wand-magic"></i>
Auto
</div>
<small>
Automatically select a background based on the chat context.<br>
Respects the "Lock" setting state.
</small>
</div>
<div>Preview</div>
<div id="custom_bg_preview">
</div>
</div>
</div>
</div>
`;
$('#extensions_settings').append(html);
$('#lock_background').on('click', onLockBackgroundClick);
$('#unlock_background').on('click', onUnlockBackgroundClick);
$(document).on("click", ".bg_example", onSelectBackgroundClick);
$('#auto_background').on("click", autoBackgroundCommand);
}
addSettings();
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], " locks a background for the currently selected chat", true, true);
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' unlocks a background for the currently selected chat', true, true);
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' automatically changes the background based on the chat context using the AI request prompt', true, true);
eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
eventSource.on(event_types.CHAT_CHANGED, moduleWorker);
});

View File

@ -1,11 +0,0 @@
{
"display_name": "Chat Backgrounds",
"loading_order": 7,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,45 +0,0 @@
#custom_bg_preview {
width: 160px;
height: 90px;
background-color: var(--grey30a);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
border-radius: 20px;
border: 1px solid var(--SmartThemeBorderColor);
box-shadow: 0 0 7px var(--black50a);
margin: 5px;
}
#custom_bg_preview::before {
content: 'No Background';
color: white;
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#custom_bg_preview:not([style*="background-image: none"])::before {
display: none;
}
.background_controls .menu_button {
display: flex;
flex-direction: row;
align-items: center;
column-gap: 10px;
}
.background_controls {
display: flex;
flex-direction: row;
align-items: center;
column-gap: 10px;
}
.background_controls small {
flex-grow: 1;
}

View File

@ -1,11 +0,0 @@
{
"display_name": "Bulk Card Editor",
"loading_order": 9,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "city-unit",
"version": "1.0.0",
"homePage": "https://github.com/city-unit"
}

View File

@ -1,7 +0,0 @@
.bulk_select_checkbox {
align-self: center;
}
#rm_print_characters_block.bulk_select .wide100pLess70px {
width: calc(100% - 85px);
}

View File

@ -1,11 +0,0 @@
{
"display_name": "CFG",
"loading_order": 1,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "kingbri",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,4 +0,0 @@
<a id="option_toggle_CFG">
<i class="fa-lg fa-solid fa-scale-balanced"></i>
<span data-i18n="CFG Scale">CFG Scale</span>
</a>

View File

@ -1,90 +0,0 @@
import { chat_metadata, substituteParams, this_chid } from "../../../script.js";
import { extension_settings, getContext } from "../../extensions.js"
import { selected_group } from "../../group-chats.js";
import { getCharaFilename } from "../../utils.js";
export const cfgType = {
chat: 0,
chara: 1,
global: 2
}
export const metadataKeys = {
guidance_scale: "cfg_guidance_scale",
negative_prompt: "cfg_negative_prompt",
positive_prompt: "cfg_positive_prompt",
prompt_combine: "cfg_prompt_combine",
groupchat_individual_chars: "cfg_groupchat_individual_chars",
prompt_insertion_depth: "cfg_prompt_insertion_depth",
prompt_separator: "cfg_prompt_separator"
}
// Gets the CFG guidance scale
// If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
export function getGuidanceScale() {
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) {
return {
type: cfgType.chat,
value: chatGuidanceScale
};
}
if ((!selected_group && charaCfg || groupchatCharOverride) && charaCfg?.guidance_scale !== 1) {
return {
type: cfgType.chara,
value: charaCfg.guidance_scale
};
}
if (extension_settings.cfg.global && extension_settings.cfg.global?.guidance_scale !== 1) {
return {
type: cfgType.global,
value: extension_settings.cfg.global.guidance_scale
};
}
}
// Gets the CFG prompt
export function getCfgPrompt(guidanceScale, isNegative) {
let splitCfgPrompt = [];
const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? [];
if (guidanceScale.type === cfgType.chat || cfgPromptCombine.includes(cfgType.chat)) {
splitCfgPrompt.unshift(
substituteParams(
chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt]
)
);
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
if (guidanceScale.type === cfgType.chara || cfgPromptCombine.includes(cfgType.chara)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt
)
);
}
if (guidanceScale.type === cfgType.global || cfgPromptCombine.includes(cfgType.global)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt
)
);
}
// This line is a bit hacky with a JSON.stringify and JSON.parse. Fix this if possible.
const customSeparator = JSON.parse(chat_metadata[metadataKeys.prompt_separator] || JSON.stringify("\n")) ?? "\n";
const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator);
const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1;
console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
return {
value: combinedCfgPrompt,
depth: insertionDepth
};
}

View File

@ -1,172 +0,0 @@
<div id="cfgConfig" class="drawer-content flexGap5">
<div class="panelControlBar flex-container">
<div id="cfgConfigHeader" class="fa-solid fa-grip drag-grabber"></div>
<div id="CFGClose" class="fa-solid fa-circle-xmark"></div>
</div>
<div name="cfgConfigHolder" class="scrollY">
<div id="chat_cfg_container">
<div class="inline-drawer">
<div id="CFGBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Chat CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small>
<b>Unique to this chat.</b><br>
</small>
<label for="chat_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="chat_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="chat_cfg_guidance_scale" id="chat_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="chat_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="chat_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="chat_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="chat_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
<div id="groupchat_cfg_use_chara_container">
<label class="checkbox_label" for="groupchat_cfg_use_chara">
<input type="checkbox" id="groupchat_cfg_use_chara" />
<span data-i18n="Use character CFG scales">Use character CFG scales</span>
</label>
</div>
</div>
</div>
</div>
<div id="chara_cfg_container" style="display: none;">
<hr class="sysHR">
<div class="inline-drawer">
<div id="charaANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Character CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><b>Will be automatically added as the CFG for this character.</b></small>
<br />
<label for="chara_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="chara_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="chara_cfg_guidance_scale" id="chara_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="chara_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="chara_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="chara_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="chara_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
</div>
</div>
</div>
<div id="global_cfg_container">
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Global CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><b>Will be used as the default CFG options for every chat unless overridden.</b></small>
<br />
<label for="global_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="global_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="global_cfg_guidance_scale" id="global_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="global_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="global_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="global_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="global_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
</div>
</div>
</div>
<div id="cfg_prompt_combine_container">
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>CFG Prompt Cascading</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="flex-container flexFlowColumn">
<small>
<b>Combine positive/negative prompts from other boxes.</b>
<br />
For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.
</small>
</div>
<br />
<div class="flex-container flexFlowColumn">
<label for="cfg_prompt_combine">
<span data-i18n="Scale">Always Include</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="0" />
<span data-i18n="Chat Negatives">Chat Negatives</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="1" />
<span data-i18n="Character Negatives">Character Negatives</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="2" />
<span data-i18n="Global Negatives">Global Negatives</span>
</label>
</div>
<div class="flex-container flexFlowColumn">
<label>
Custom Separator: <input id="cfg_prompt_separator" class="text_pole textarea_compact widthUnset" placeholder="&quot;\n&quot;" type="text" />
</label>
<label>
Insertion Depth: <input id="cfg_prompt_insertion_depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,88 +0,0 @@
import { callPopup } from "../../../script.js";
import { getContext } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
export { MODULE_NAME };
const MODULE_NAME = 'dice';
const UPDATE_INTERVAL = 1000;
async function doDiceRoll(customDiceFormula) {
let value = typeof customDiceFormula === 'string' ? customDiceFormula.trim() : $(this).data('value');
if (value == 'custom') {
value = await callPopup('Enter the dice formula:<br><i>(for example, <tt>2d6</tt>)</i>', 'input');
}
if (!value) {
return;
}
const isValid = droll.validate(value);
if (isValid) {
const result = droll.roll(value);
const context = getContext();
context.sendSystemMessage('generic', `${context.name1} rolls a ${value}. The result is: ${result.total} (${result.rolls})`, { isSmallSys: true });
} else {
toastr.warning('Invalid dice formula');
}
}
function addDiceRollButton() {
const buttonHtml = `
<div id="roll_dice" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-dice extensionsMenuExtensionButton" title="Roll Dice" /></div>
Roll Dice
</div>
`;
const dropdownHtml = `
<div id="dice_dropdown">
<ul class="list-group">
<li class="list-group-item" data-value="d4">d4</li>
<li class="list-group-item" data-value="d6">d6</li>
<li class="list-group-item" data-value="d8">d8</li>
<li class="list-group-item" data-value="d10">d10</li>
<li class="list-group-item" data-value="d12">d12</li>
<li class="list-group-item" data-value="d20">d20</li>
<li class="list-group-item" data-value="d100">d100</li>
<li class="list-group-item" data-value="custom">...</li>
</ul>
</div>`;
$('#extensionsMenu').prepend(buttonHtml);
$(document.body).append(dropdownHtml)
$('#dice_dropdown li').on('click', doDiceRoll);
const button = $('#roll_dice');
const dropdown = $('#dice_dropdown');
dropdown.hide();
button.hide();
let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
placement: 'top',
});
$(document).on('click touchend', function (e) {
const target = $(e.target);
if (target.is(dropdown)) return;
if (target.is(button) && !dropdown.is(":visible")) {
e.preventDefault();
dropdown.fadeIn(250);
popper.update();
} else {
dropdown.fadeOut(250);
}
});
}
async function moduleWorker() {
$('#roll_dice').toggle(getContext().onlineStatus !== 'no_connection');
}
jQuery(function () {
addDiceRollButton();
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
registerSlashCommand('roll', (_, value) => doDiceRoll(value), ['r'], "<span class='monospace'>(dice formula)</span> roll the dice. For example, /roll 2d6", false, true);
});

View File

@ -1,11 +0,0 @@
{
"display_name": "D&D Dice",
"loading_order": 5,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,26 +0,0 @@
#roll_dice {
/* order: 100; */
/* width: 40px;
height: 40px;
margin: 0;
padding: 1px; */
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
display: flex;
align-items: center;
/* justify-content: center; */
}
#roll_dice:hover {
opacity: 1;
filter: brightness(1.2);
}
#dice_dropdown {
z-index: 30000;
backdrop-filter: blur(--SmartThemeBlurStrength);
}

View File

@ -1,4 +1,4 @@
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js"; import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced, this_chid } from "../../../script.js";
import { dragElement, isMobile } from "../../RossAscends-mods.js"; import { dragElement, isMobile } from "../../RossAscends-mods.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplate } from "../../extensions.js"; import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplate } from "../../extensions.js";
import { loadMovingUIState, power_user } from "../../power-user.js"; import { loadMovingUIState, power_user } from "../../power-user.js";
@ -493,24 +493,6 @@ async function moduleWorker() {
return; return;
} }
// character changed
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
removeExpression();
spriteCache = {};
//clear expression
let imgElement = document.getElementById('expression-image');
if (imgElement && imgElement instanceof HTMLImageElement) {
imgElement.src = "";
}
//set checkbox to global var
$('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead);
if (extension_settings.expressions.talkinghead) {
setTalkingHeadState(extension_settings.expressions.talkinghead);
}
}
const vnMode = isVisualNovelMode(); const vnMode = isVisualNovelMode();
const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible'); const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible');
@ -531,7 +513,7 @@ async function moduleWorker() {
} }
const currentLastMessage = getLastCharacterMessage(); const currentLastMessage = getLastCharacterMessage();
let spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name); let spriteFolderName = context.groupId ? getSpriteFolderName(currentLastMessage, currentLastMessage.name) : getSpriteFolderName();
// character has no expressions or it is not loaded // character has no expressions or it is not loaded
if (Object.keys(spriteCache).length === 0) { if (Object.keys(spriteCache).length === 0) {
@ -782,7 +764,7 @@ function sampleClassifyText(text) {
// Remove asterisks and quotes // Remove asterisks and quotes
let result = text.replace(/[\*\"]/g, ''); let result = text.replace(/[\*\"]/g, '');
const SAMPLE_THRESHOLD = 300; const SAMPLE_THRESHOLD = 500;
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2; const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
if (text.length < SAMPLE_THRESHOLD) { if (text.length < SAMPLE_THRESHOLD) {
@ -1492,11 +1474,32 @@ function setExpressionOverrideHtml(forceClear = false) {
moduleWorker(); moduleWorker();
dragElement($("#expression-holder")) dragElement($("#expression-holder"))
eventSource.on(event_types.CHAT_CHANGED, () => { eventSource.on(event_types.CHAT_CHANGED, () => {
// character changed
const context = getContext();
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
removeExpression();
spriteCache = {};
//clear expression
let imgElement = document.getElementById('expression-image');
if (imgElement && imgElement instanceof HTMLImageElement) {
imgElement.src = "";
}
//set checkbox to global var
$('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead);
if (extension_settings.expressions.talkinghead) {
setTalkingHeadState(extension_settings.expressions.talkinghead);
}
}
setExpressionOverrideHtml(); setExpressionOverrideHtml();
if (isVisualNovelMode()) { if (isVisualNovelMode()) {
$('#visual-novel-wrapper').empty(); $('#visual-novel-wrapper').empty();
} }
updateFunction();
}); });
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced); eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);

View File

@ -5,7 +5,7 @@ import {
getRequestHeaders, getRequestHeaders,
} from "../../../script.js"; } from "../../../script.js";
import { selected_group } from "../../group-chats.js"; import { selected_group } from "../../group-chats.js";
import { loadFileToDocument } from "../../utils.js"; import { loadFileToDocument, delay } from "../../utils.js";
import { loadMovingUIState } from '../../power-user.js'; import { loadMovingUIState } from '../../power-user.js';
import { dragElement } from '../../RossAscends-mods.js'; import { dragElement } from '../../RossAscends-mods.js';
import { registerSlashCommand } from "../../slash-commands.js"; import { registerSlashCommand } from "../../slash-commands.js";
@ -109,6 +109,13 @@ async function initGallery(items, url) {
let file = e.originalEvent.dataTransfer.files[0]; let file = e.originalEvent.dataTransfer.files[0];
uploadFile(file, url); // Added url parameter to know where to upload uploadFile(file, url); // Added url parameter to know where to upload
}); });
//let images populate first
await delay(100)
//unset the height (which must be getting set by the gallery library at some point)
$("#dragGallery").css('height', 'unset');
//force a resize to make images display correctly
jQuery("#dragGallery").nanogallery2('resize');
} }
/** /**
@ -247,14 +254,16 @@ $(document).ready(function () {
* The cloned element has its attributes set, a new child div appended, and is made visible on the body. * The cloned element has its attributes set, a new child div appended, and is made visible on the body.
* Additionally, it sets up the element to prevent dragging on its images. * Additionally, it sets up the element to prevent dragging on its images.
*/ */
function makeMovable(id="gallery"){ function makeMovable(id = "gallery") {
console.debug('making new container from template') console.debug('making new container from template')
const template = $('#generic_draggable_template').html(); const template = $('#generic_draggable_template').html();
const newElement = $(template); const newElement = $(template);
newElement.css('background-color', 'var(--SmartThemeBlurTintColor)');
newElement.attr('forChar', id); newElement.attr('forChar', id);
newElement.attr('id', `${id}`); newElement.attr('id', `${id}`);
newElement.find('.drag-grabber').attr('id', `${id}header`); newElement.find('.drag-grabber').attr('id', `${id}header`);
newElement.find('.dragTitle').text('Image Gallery')
//add a div for the gallery //add a div for the gallery
newElement.append(`<div id="dragGallery"></div>`); newElement.append(`<div id="dragGallery"></div>`);
// add no-scrollbar class to this element // add no-scrollbar class to this element
@ -326,6 +335,8 @@ function makeDragImg(id, url) {
// Ensure that the newly added element is displayed as block // Ensure that the newly added element is displayed as block
draggableElem.style.display = 'block'; draggableElem.style.display = 'block';
//and has no padding unlike other non-zoomed-avatar draggables
draggableElem.style.padding = '0';
// Add an id to the close button // Add an id to the close button
// If the close button exists, set related-id // If the close button exists, set related-id
@ -375,11 +386,11 @@ function makeDragImg(id, url) {
* @param {string} id - The ID to be sanitized. * @param {string} id - The ID to be sanitized.
* @returns {string} - The sanitized ID. * @returns {string} - The sanitized ID.
*/ */
function sanitizeHTMLId(id){ function sanitizeHTMLId(id) {
// Replace spaces and non-word characters // Replace spaces and non-word characters
id = id.replace(/\s+/g, '-') id = id.replace(/\s+/g, '-')
.replace(/[^\x00-\x7F]/g, '-') .replace(/[^\x00-\x7F]/g, '-')
.replace(/\W/g, ''); .replace(/\W/g, '');
return id; return id;
} }

View File

@ -1,210 +0,0 @@
import { eventSource, event_types, getRequestHeaders, is_send_press, saveSettingsDebounced } from "../../../script.js";
import { extension_settings, getContext, renderExtensionTemplate } from "../../extensions.js";
import { SECRET_KEYS, secret_state } from "../../secrets.js";
import { collapseNewlines } from "../../power-user.js";
import { bufferToBase64, debounce } from "../../utils.js";
import { decodeTextTokens, getTextTokens, tokenizers } from "../../tokenizers.js";
const MODULE_NAME = 'hypebot';
const WAITING_VERBS = ['thinking', 'typing', 'brainstorming', 'cooking', 'conjuring'];
const MAX_PROMPT = 1024;
const MAX_LENGTH = 50;
const MAX_STRING_LENGTH = MAX_PROMPT * 4;
const settings = {
enabled: false,
name: 'Goose',
};
/**
* Returns a random waiting verb
* @returns {string} Random waiting verb
*/
function getWaitingVerb() {
return WAITING_VERBS[Math.floor(Math.random() * WAITING_VERBS.length)];
}
/**
* Returns a random verb based on the text
* @param {string} text Text to generate a verb for
* @returns {string} Random verb
*/
function getVerb(text) {
let verbList = ['says', 'notes', 'states', 'whispers', 'murmurs', 'mumbles'];
if (text.endsWith('!')) {
verbList = ['proclaims', 'declares', 'salutes', 'exclaims', 'cheers'];
}
if (text.endsWith('?')) {
verbList = ['asks', 'suggests', 'ponders', 'wonders', 'inquires', 'questions'];
}
return verbList[Math.floor(Math.random() * verbList.length)];
}
/**
* Formats the HypeBot reply text
* @param {string} text HypeBot output text
* @returns {string} Formatted HTML text
*/
function formatReply(text) {
return `<span class="hypebot_name">${settings.name} ${getVerb(text)}:</span>&nbsp;<span class="hypebot_text">${text}</span>`;
}
let hypeBotBar;
let abortController;
const generateDebounced = debounce(() => generateHypeBot(), 500);
/**
* Sets the HypeBot text. Preserves scroll position of the chat.
* @param {string} text Text to set
*/
function setHypeBotText(text) {
const chatBlock = $('#chat');
const originalScrollBottom = chatBlock[0].scrollHeight - (chatBlock.scrollTop() + chatBlock.outerHeight());
hypeBotBar.html(DOMPurify.sanitize(text));
const newScrollTop = chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom);
chatBlock.scrollTop(newScrollTop);
}
/**
* Called when a chat event occurs to generate a HypeBot reply.
* @param {boolean} clear Clear the hypebot bar.
*/
function onChatEvent(clear) {
if (clear) {
setHypeBotText('');
}
abortController?.abort();
generateDebounced();
};
/**
* Generates a HypeBot reply.
*/
async function generateHypeBot() {
if (!settings.enabled || is_send_press) {
return;
}
if (!secret_state[SECRET_KEYS.NOVEL]) {
setHypeBotText('<div class="hypebot_nokey">No API key found. Please enter your API key in the NovelAI API Settings to use the HypeBot.</div>');
return;
}
console.debug('Generating HypeBot reply');
setHypeBotText(`<span class="hypebot_name">${settings.name}</span> is ${getWaitingVerb()}...`);
const context = getContext();
const chat = context.chat.slice();
let prompt = '';
for (let index = chat.length - 1; index >= 0; index--) {
const message = chat[index];
if (message.is_system || !message.mes) {
continue;
}
prompt = `\n${message.mes}\n${prompt}`;
if (prompt.length >= MAX_STRING_LENGTH) {
break;
}
}
prompt = collapseNewlines(prompt.replaceAll(/[\*\[\]\{\}]/g, ''));
if (!prompt) {
return;
}
const sliceLength = MAX_PROMPT - MAX_LENGTH;
const encoded = getTextTokens(tokenizers.GPT2, prompt).slice(-sliceLength);
// Add a stop string token to the end of the prompt
encoded.push(49527);
const base64String = await bufferToBase64(new Uint16Array(encoded).buffer);
const parameters = {
input: base64String,
model: "hypebot",
streaming: false,
temperature: 1,
max_length: MAX_LENGTH,
min_length: 1,
top_k: 0,
top_p: 1,
tail_free_sampling: 0.95,
repetition_penalty: 1,
repetition_penalty_range: 2048,
repetition_penalty_slope: 0.18,
repetition_penalty_frequency: 0,
repetition_penalty_presence: 0,
phrase_rep_pen: "off",
bad_words_ids: [],
stop_sequences: [[48585]],
generate_until_sentence: true,
use_cache: false,
use_string: false,
return_full_text: false,
prefix: "vanilla",
logit_bias_exp: [],
order: [0, 1, 2, 3],
};
abortController = new AbortController();
const response = await fetch('/api/novelai/generate', {
headers: getRequestHeaders(),
body: JSON.stringify(parameters),
method: 'POST',
signal: abortController.signal,
});
if (response.ok) {
const data = await response.json();
const ids = Array.from(new Uint16Array(Uint8Array.from(atob(data.output), c => c.charCodeAt(0)).buffer));
const output = decodeTextTokens(tokenizers.GPT2, ids).replace(/<2F>/g, '').trim();
setHypeBotText(formatReply(output));
} else {
setHypeBotText('<div class="hypebot_error">Something went wrong while generating a HypeBot reply. Please try again.</div>');
}
}
jQuery(() => {
if (!extension_settings.hypebot) {
extension_settings.hypebot = settings;
}
Object.assign(settings, extension_settings.hypebot);
$('#extensions_settings2').append(renderExtensionTemplate(MODULE_NAME, 'settings'));
hypeBotBar = $(`<div id="hypeBotBar"></div>`).toggle(settings.enabled);
$('#send_form').append(hypeBotBar);
$('#hypebot_enabled').prop('checked', settings.enabled).on('input', () => {
settings.enabled = $('#hypebot_enabled').prop('checked');
hypeBotBar.toggle(settings.enabled);
abortController?.abort();
Object.assign(extension_settings.hypebot, settings);
saveSettingsDebounced();
});
$('#hypebot_name').val(settings.name).on('input', () => {
settings.name = String($('#hypebot_name').val());
Object.assign(extension_settings.hypebot, settings);
saveSettingsDebounced();
});
eventSource.on(event_types.CHAT_CHANGED, () => onChatEvent(true));
eventSource.on(event_types.MESSAGE_DELETED, () => onChatEvent(true));
eventSource.on(event_types.MESSAGE_EDITED, () => onChatEvent(true));
eventSource.on(event_types.MESSAGE_SENT, () => onChatEvent(false));
eventSource.on(event_types.MESSAGE_RECEIVED, () => onChatEvent(false));
eventSource.on(event_types.MESSAGE_SWIPED, () => onChatEvent(false));
});

View File

@ -1,11 +0,0 @@
{
"display_name": "HypeBot",
"loading_order": 1000,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,18 +0,0 @@
<div class="hypebot_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>HypeBot</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>Show personalized suggestions based on your recent chats using the NovelAI's HypeBot engine.</div>
<small><i>Hint: Save an API key in the NovelAI API settings to use it here.</i></small>
<label class="checkbox_label" for="hypebot_enabled">
<input id="hypebot_enabled" type="checkbox" class="checkbox">
Enabled
</label>
<label>Name:</label>
<input id="hypebot_name" type="text" class="text_pole" placeholder="Goose">
</div>
</div>
</div>

View File

@ -1,17 +0,0 @@
#hypeBotBar {
width: 100%;
max-width: 100%;
padding: 0.5em;
white-space: normal;
font-size: calc(var(--mainFontSize) * 0.85);
order: 20;
}
.hypebot_nokey {
text-align: center;
font-style: italic;
}
.hypebot_name {
font-weight: 600;
}

View File

@ -1,54 +0,0 @@
<div class="idle-settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header" title="Indicates the settings for the idle feature.">
<b>Idle</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="idle_block flex-container">
<input id="idle_enabled" type="checkbox" title="Toggle to enable or disable the idle feature." />
<label for="idle_enabled">Enabled</label>
</div>
<div class="idle_block flex-container">
<input id="idle_repeats" class="text_pole widthUnset" type="number" min="0" max="100000" step="1" title="The number of times the idle action will be prompted." />
<label for="idle_repeats">Idle Prompt Count</label>
</div>
<div class="idle_block flex-container" style="display: none;">
<input id="idle_timer_min" class="text_pole widthUnset" type="number" min="0" max="600000" step="1" title="The minimum amount of time in seconds before the idle action is triggered." />
<label for="idle_timer_min">Idle Timer Minimum (seconds)</label>
</div>
<div class="idle_block flex-container">
<input id="idle_timer" class="text_pole widthUnset" type="number" min="0" max="600000" step="1" title="The amount of time in seconds before the idle action is triggered." />
<label for="idle_timer">Idle Timer (seconds)</label>
</div>
<div class="idle_block flex-container">
<label for="idle_prompts">Idle Prompts</label>
<textarea id="idle_prompts" class="text_pole textarea_compact" rows="6" title="The prompts to be sent to initial the idle reply (newline seperated)."></textarea>
</div>
<div class="idle_block flex-container">
<input id="idle_use_continuation" type="checkbox" title="Indicates whether the idle action will just use the 'Continue' function instead of a prompt." />
<label for="idle_use_continuation">Use Continuation</label>
</div>
<div class="idle_block flex-container">
<input id="idle_random_time" type="checkbox" title="Indicates if the idle time should be randomized between a min/max value." />
<label for="idle_random_time">Randomize Time</label>
</div>
<div class="idle_block flex-container">
<input id="idle_include_prompt" type="checkbox" title="Indicates if the idle prompting should be included in context. (Sends as user)" />
<label for="idle_include_prompt">Include Idle Prompt</label>
</div>
<div class="idle_block flex-container">
<label for="idle_sendAs">Send As</label>
<select id="idle_sendAs" class="text_pole" title="Determines how the idle message prompting is sent; as a user, character, system, or raw message.">
<option value="user">User</option>
<option value="char">Character</option>
<option value="sys">System</option>
<option value="raw">Raw</option>
</select>
</div>
<hr class="sysHR" />
</div>
</div>
</div>

View File

@ -1,329 +0,0 @@
import {
saveSettingsDebounced,
substituteParams
} from "../../../script.js";
import { debounce } from "../../utils.js";
import { promptQuietForLoudResponse, sendMessageAs, sendNarratorMessage } from "../../slash-commands.js";
import { extension_settings, getContext, renderExtensionTemplate } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
const extensionName = "idle";
let idleTimer = null;
let repeatCount = 0;
let defaultSettings = {
enabled: false,
timer: 120,
prompts: [
"*stands silently, looking deep in thought*",
"*pauses, eyes wandering over the surroundings*",
"*hesitates, appearing lost for a moment*",
"*takes a deep breath, collecting their thoughts*",
"*gazes into the distance, seemingly distracted*",
"*remains still, absorbing the ambiance*",
"*lingers in silence, a contemplative look on their face*",
"*stops, fingers brushing against an old memory*",
"*seems to drift into a momentary daydream*",
"*waits quietly, allowing the weight of the moment to settle*",
],
useContinuation: true,
repeats: 2, // 0 = infinite
sendAs: "user",
randomTime: false,
timeMin: 60,
includePrompt: false,
};
//TODO: Can we make this a generic function?
/**
* Load the extension settings and set defaults if they don't exist.
*/
async function loadSettings() {
if (!extension_settings.idle) {
console.log("Creating extension_settings.idle");
extension_settings.idle = {};
}
for (const [key, value] of Object.entries(defaultSettings)) {
if (!extension_settings.idle.hasOwnProperty(key)) {
console.log(`Setting default for: ${key}`);
extension_settings.idle[key] = value;
}
}
populateUIWithSettings();
}
//TODO: Can we make this a generic function too?
/**
* Populate the UI components with values from the extension settings.
*/
function populateUIWithSettings() {
$("#idle_timer").val(extension_settings.idle.timer).trigger("input");
$("#idle_prompts").val(extension_settings.idle.prompts.join("\n")).trigger("input");
$("#idle_use_continuation").prop("checked", extension_settings.idle.useContinuation).trigger("input");
$("#idle_enabled").prop("checked", extension_settings.idle.enabled).trigger("input");
$("#idle_repeats").val(extension_settings.idle.repeats).trigger("input");
$("#idle_sendAs").val(extension_settings.idle.sendAs).trigger("input");
$("#idle_random_time").prop("checked", extension_settings.idle.randomTime).trigger("input");
$("#idle_timer_min").val(extension_settings.idle.timerMin).trigger("input");
$("#idle_include_prompt").prop("checked", extension_settings.idle.includePrompt).trigger("input");
}
/**
* Reset the idle timer based on the extension settings and context.
*/
function resetIdleTimer() {
console.debug("Resetting idle timer");
if (idleTimer) clearTimeout(idleTimer);
let context = getContext();
if (!context.characterId && !context.groupID) return;
if (!extension_settings.idle.enabled) return;
if (extension_settings.idle.randomTime) {
// ensure these are ints
let min = extension_settings.idle.timerMin;
let max = extension_settings.idle.timer;
min = parseInt(min);
max = parseInt(max);
let randomTime = (Math.random() * (max - min + 1)) + min;
idleTimer = setTimeout(sendIdlePrompt, 1000 * randomTime);
} else {
idleTimer = setTimeout(sendIdlePrompt, 1000 * extension_settings.idle.timer);
}
}
/**
* Send a random idle prompt to the AI based on the extension settings.
* Checks conditions like if the extension is enabled and repeat conditions.
*/
async function sendIdlePrompt() {
if (!extension_settings.idle.enabled) return;
// Check repeat conditions and waiting for a response
if (repeatCount >= extension_settings.idle.repeats || $('#mes_stop').is(':visible')) {
//console.debug("Not sending idle prompt due to repeat conditions or waiting for a response.");
resetIdleTimer();
return;
}
const randomPrompt = extension_settings.idle.prompts[
Math.floor(Math.random() * extension_settings.idle.prompts.length)
];
sendPrompt(randomPrompt);
repeatCount++;
resetIdleTimer();
}
/**
* Add our prompt to the chat and then send the chat to the backend.
* @param {string} sendAs - The type of message to send. "user", "char", or "sys".
* @param {string} prompt - The prompt text to send to the AI.
*/
function sendLoud(sendAs, prompt) {
if (sendAs === "user") {
prompt = substituteParams(prompt);
$("#send_textarea").val(prompt);
// Set the focus back to the textarea
$("#send_textarea").focus();
$("#send_but").trigger('click');
} else if (sendAs === "char") {
sendMessageAs("", `${getContext().name2}\n${prompt}`);
promptQuietForLoudResponse(sendAs, "");
} else if (sendAs === "sys") {
sendNarratorMessage("", prompt);
promptQuietForLoudResponse(sendAs, "");
}
else {
console.error(`Unknown sendAs value: ${sendAs}`);
}
}
/**
* Send the provided prompt to the AI. Determines method based on continuation setting.
* @param {string} prompt - The prompt text to send to the AI.
*/
function sendPrompt(prompt) {
clearTimeout(idleTimer);
$("#send_textarea").off("input");
if (extension_settings.idle.useContinuation) {
$('#option_continue').trigger('click');
console.debug("Sending idle prompt with continuation");
} else {
console.debug("Sending idle prompt");
console.log(extension_settings.idle);
if (extension_settings.idle.includePrompt) {
sendLoud(extension_settings.idle.sendAs, prompt);
}
else {
promptQuietForLoudResponse(extension_settings.idle.sendAs, prompt);
}
}
}
/**
* Load the settings HTML and append to the designated area.
*/
async function loadSettingsHTML() {
const settingsHtml = renderExtensionTemplate(extensionName, "dropdown");
$("#extensions_settings2").append(settingsHtml);
}
/**
* Update a specific setting based on user input.
* @param {string} elementId - The HTML element ID tied to the setting.
* @param {string} property - The property name in the settings object.
* @param {boolean} [isCheckbox=false] - Whether the setting is a checkbox.
*/
function updateSetting(elementId, property, isCheckbox = false) {
let value = $(`#${elementId}`).val();
if (isCheckbox) {
value = $(`#${elementId}`).prop('checked');
}
if (property === "prompts") {
value = value.split("\n");
}
extension_settings.idle[property] = value;
saveSettingsDebounced();
}
/**
* Attach an input listener to a UI component to update the corresponding setting.
* @param {string} elementId - The HTML element ID tied to the setting.
* @param {string} property - The property name in the settings object.
* @param {boolean} [isCheckbox=false] - Whether the setting is a checkbox.
*/
function attachUpdateListener(elementId, property, isCheckbox = false) {
$(`#${elementId}`).on('input', debounce(() => {
updateSetting(elementId, property, isCheckbox);
}, 250));
}
/**
* Handle the enabling or disabling of the idle extension.
* Adds or removes the idle listeners based on the checkbox's state.
*/
function handleIdleEnabled() {
if (!extension_settings.idle.enabled) {
clearTimeout(idleTimer);
removeIdleListeners();
} else {
resetIdleTimer();
attachIdleListeners();
}
}
/**
* Setup input listeners for the various settings and actions related to the idle extension.
*/
function setupListeners() {
const settingsToWatch = [
['idle_timer', 'timer'],
['idle_prompts', 'prompts'],
['idle_use_continuation', 'useContinuation', true],
['idle_enabled', 'enabled', true],
['idle_repeats', 'repeats'],
['idle_sendAs', 'sendAs'],
['idle_random_time', 'randomTime', true],
['idle_timer_min', 'timerMin'],
['idle_include_prompt', 'includePrompt', true]
];
settingsToWatch.forEach(setting => {
attachUpdateListener(...setting);
});
// Idleness listeners, could be made better
$('#idle_enabled').on('input', debounce(handleIdleEnabled, 250));
// Add the idle listeners initially if the idle feature is enabled
if (extension_settings.idle.enabled) {
attachIdleListeners();
}
//show/hide timer min parent div
$('#idle_random_time').on('input', function () {
if ($(this).prop('checked')) {
$('#idle_timer_min').parent().show();
} else {
$('#idle_timer_min').parent().hide();
}
$('#idle_timer').trigger('input');
});
// if we're including the prompt, hide raw from the sendAs dropdown
$('#idle_include_prompt').on('input', function () {
if ($(this).prop('checked')) {
$('#idle_sendAs option[value="raw"]').hide();
} else {
$('#idle_sendAs option[value="raw"]').show();
}
});
//make sure timer min is less than timer
$('#idle_timer').on('input', function () {
if ($('#idle_random_time').prop('checked')) {
if ($(this).val() < $('#idle_timer_min').val()) {
$('#idle_timer_min').val($(this).val());
$('#idle_timer_min').trigger('input');
}
}
});
}
const debouncedActivityHandler = debounce((event) => {
// Check if the event target (or any of its parents) has the id "option_continue"
if ($(event.target).closest('#option_continue').length) {
return; // Do not proceed if the click was on (or inside) an element with id "option_continue"
}
console.debug("Activity detected, resetting idle timer");
resetIdleTimer();
repeatCount = 0;
}, 250);
function attachIdleListeners() {
$(document).on("click keypress", debouncedActivityHandler);
document.addEventListener('keydown', debouncedActivityHandler);
}
/**
* Remove idle-specific listeners.
*/
function removeIdleListeners() {
$(document).off("click keypress", debouncedActivityHandler);
document.removeEventListener('keydown', debouncedActivityHandler);
}
function toggleIdle() {
extension_settings.idle.enabled = !extension_settings.idle.enabled;
$('#idle_enabled').prop('checked', extension_settings.idle.enabled);
$('#idle_enabled').trigger('input');
toastr.info(`Idle mode ${extension_settings.idle.enabled ? "enabled" : "disabled"}.`);
resetIdleTimer();
}
jQuery(async () => {
await loadSettingsHTML();
loadSettings();
setupListeners();
if (extension_settings.idle.enabled) {
resetIdleTimer();
}
// once the doc is ready, check if random time is checked and hide/show timer min
if ($('#idle_random_time').prop('checked')) {
$('#idle_timer_min').parent().show();
}
registerSlashCommand('idle', toggleIdle, [], ' toggles idle mode', true, true);
});

View File

@ -1,12 +0,0 @@
{
"display_name": "Idle",
"loading_order": 6,
"requires": [],
"optional": [
],
"js": "index.js",
"css": "style.css",
"author": "City-Unit",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,3 +0,0 @@
.idle_block {
align-items: center;
}

View File

@ -1,949 +0,0 @@
import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, substituteParams, } from "../../../script.js";
import { humanizedDateTime } from "../../RossAscends-mods.js";
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js";
import { CHARACTERS_PER_TOKEN_RATIO } from "../../tokenizers.js";
import { getFileText, onlyUnique, splitRecursive } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'chromadb';
const dbStore = localforage.createInstance({ name: 'SillyTavern_ChromaDB' });
const defaultSettings = {
strategy: 'original',
sort_strategy: 'date',
keep_context: 10,
keep_context_min: 1,
keep_context_max: 500,
keep_context_step: 1,
n_results: 20,
n_results_min: 0,
n_results_max: 500,
n_results_step: 1,
chroma_depth: 20,
chroma_depth_min: -1,
chroma_depth_max: 500,
chroma_depth_step: 1,
chroma_default_msg: "In a past conversation: [{{memories}}]",
chroma_default_hhaa_wrapper: "Previous messages exchanged between {{user}} and {{char}}:\n{{memories}}",
chroma_default_hhaa_memory: "- {{name}}: {{message}}\n",
hhaa_token_limit: 512,
split_length: 384,
split_length_min: 64,
split_length_max: 4096,
split_length_step: 64,
file_split_length: 1024,
file_split_length_min: 512,
file_split_length_max: 4096,
file_split_length_step: 128,
keep_context_proportion: 0.5,
keep_context_proportion_min: 0.0,
keep_context_proportion_max: 1.0,
keep_context_proportion_step: 0.05,
auto_adjust: true,
freeze: false,
query_last_only: true,
};
const postHeaders = {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
};
async function invalidateMessageSyncState(messageId) {
console.log('CHROMADB: invalidating message sync state', messageId);
const state = await getChatSyncState();
state[messageId] = 0;
await dbStore.setItem(getCurrentChatId(), state);
}
async function getChatSyncState() {
const currentChatId = getCurrentChatId();
if (!checkChatId(currentChatId)) {
return;
}
const context = getContext();
const chatState = (await dbStore.getItem(currentChatId)) || [];
// if the chat length has decreased, it means that some messages were deleted
if (chatState.length > context.chat.length) {
for (let i = context.chat.length; i < chatState.length; i++) {
// if the synced message was deleted, notify the user
if (chatState[i]) {
toastr.warning(
'Purge your ChromaDB to remove it from there too. See the "Smart Context" tab in the Extensions menu for more information.',
'Message deleted from chat, but it still exists inside the ChromaDB database.',
{ timeOut: 0, extendedTimeOut: 0, preventDuplicates: true },
);
break;
}
}
}
chatState.length = context.chat.length;
for (let i = 0; i < chatState.length; i++) {
if (chatState[i] === undefined) {
chatState[i] = 0;
}
}
await dbStore.setItem(currentChatId, chatState);
return chatState;
}
async function loadSettings() {
if (Object.keys(extension_settings.chromadb).length === 0) {
Object.assign(extension_settings.chromadb, defaultSettings);
}
console.debug(`loading chromadb strat:${extension_settings.chromadb.strategy}`);
$("#chromadb_strategy option[value=" + extension_settings.chromadb.strategy + "]").attr(
"selected",
"true"
);
$("#chromadb_sort_strategy option[value=" + extension_settings.chromadb.sort_strategy + "]").attr(
"selected",
"true"
);
$('#chromadb_keep_context').val(extension_settings.chromadb.keep_context).trigger('input');
$('#chromadb_n_results').val(extension_settings.chromadb.n_results).trigger('input');
$('#chromadb_split_length').val(extension_settings.chromadb.split_length).trigger('input');
$('#chromadb_file_split_length').val(extension_settings.chromadb.file_split_length).trigger('input');
$('#chromadb_keep_context_proportion').val(extension_settings.chromadb.keep_context_proportion).trigger('input');
$('#chromadb_custom_depth').val(extension_settings.chromadb.chroma_depth).trigger('input');
$('#chromadb_custom_msg').val(extension_settings.chromadb.recall_msg).trigger('input');
$('#chromadb_hhaa_wrapperfmt').val(extension_settings.chromadb.hhaa_wrapper_msg).trigger('input');
$('#chromadb_hhaa_memoryfmt').val(extension_settings.chromadb.hhaa_memory_msg).trigger('input');
$('#chromadb_hhaa_token_limit').val(extension_settings.chromadb.hhaa_token_limit).trigger('input');
$('#chromadb_auto_adjust').prop('checked', extension_settings.chromadb.auto_adjust);
$('#chromadb_freeze').prop('checked', extension_settings.chromadb.freeze);
$('#chromadb_query_last_only').prop('checked', extension_settings.chromadb.query_last_only);
enableDisableSliders();
onStrategyChange();
}
function onStrategyChange() {
console.debug('changing chromadb strat');
extension_settings.chromadb.strategy = $('#chromadb_strategy').val();
if (extension_settings.chromadb.strategy === "custom") {
$('#chromadb_custom_depth').show();
$('label[for="chromadb_custom_depth"]').show();
$('#chromadb_custom_msg').show();
$('label[for="chromadb_custom_msg"]').show();
}
else if(extension_settings.chromadb.strategy === "hh_aa"){
$('#chromadb_hhaa_wrapperfmt').show();
$('label[for="chromadb_hhaa_wrapperfmt"]').show();
$('#chromadb_hhaa_memoryfmt').show();
$('label[for="chromadb_hhaa_memoryfmt"]').show();
$('#chromadb_hhaa_token_limit').show();
$('label[for="chromadb_hhaa_token_limit"]').show();
}
saveSettingsDebounced();
}
function onRecallStrategyChange() {
console.log('changing chromadb recall strat');
extension_settings.chromadb.recall_strategy = $('#chromadb_recall_strategy').val();
saveSettingsDebounced();
}
function onSortStrategyChange() {
console.log('changing chromadb sort strat');
extension_settings.chromadb.sort_strategy = $('#chromadb_sort_strategy').val();
saveSettingsDebounced();
}
function onKeepContextInput() {
extension_settings.chromadb.keep_context = Number($('#chromadb_keep_context').val());
$('#chromadb_keep_context_value').text(extension_settings.chromadb.keep_context);
saveSettingsDebounced();
}
function onNResultsInput() {
extension_settings.chromadb.n_results = Number($('#chromadb_n_results').val());
$('#chromadb_n_results_value').text(extension_settings.chromadb.n_results);
saveSettingsDebounced();
}
function onChromaDepthInput() {
extension_settings.chromadb.chroma_depth = Number($('#chromadb_custom_depth').val());
$('#chromadb_custom_depth_value').text(extension_settings.chromadb.chroma_depth);
saveSettingsDebounced();
}
function onChromaMsgInput() {
extension_settings.chromadb.recall_msg = $('#chromadb_custom_msg').val();
saveSettingsDebounced();
}
function onChromaHHAAWrapper() {
extension_settings.chromadb.hhaa_wrapper_msg = $('#chromadb_hhaa_wrapperfmt').val();
saveSettingsDebounced();
}
function onChromaHHAAMemory() {
extension_settings.chromadb.hhaa_memory_msg = $('#chromadb_hhaa_memoryfmt').val();
saveSettingsDebounced();
}
function onChromaHHAATokens() {
extension_settings.chromadb.hhaa_token_limit = Number($('#chromadb_hhaa_token_limit').val());
$('#chromadb_hhaa_token_limit_value').text(extension_settings.chromadb.hhaa_token_limit);
saveSettingsDebounced();
}
function onSplitLengthInput() {
extension_settings.chromadb.split_length = Number($('#chromadb_split_length').val());
$('#chromadb_split_length_value').text(extension_settings.chromadb.split_length);
saveSettingsDebounced();
}
function onFileSplitLengthInput() {
extension_settings.chromadb.file_split_length = Number($('#chromadb_file_split_length').val());
$('#chromadb_file_split_length_value').text(extension_settings.chromadb.file_split_length);
saveSettingsDebounced();
}
function onChunkNLInput() {
let shouldSplit = $('#onChunkNLInput').is(':checked');
if (shouldSplit) {
extension_settings.chromadb.file_split_type = "newline";
} else {
extension_settings.chromadb.file_split_type = "length";
}
saveSettingsDebounced();
}
function checkChatId(chat_id) {
if (!chat_id || chat_id.trim() === '') {
toastr.error('Please select a character and try again.');
return false;
}
return true;
}
async function addMessages(chat_id, messages) {
if (extension_settings.chromadb.freeze) {
return { count: 0 };
}
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb';
const messagesDeepCopy = JSON.parse(JSON.stringify(messages));
let splitMessages = [];
let id = 0;
messagesDeepCopy.forEach((m, index) => {
const split = splitRecursive(m.mes, extension_settings.chromadb.split_length);
splitMessages.push(...split.map(text => ({
...m,
mes: text,
send_date: id,
id: `msg-${id++}`,
index: index,
extra: undefined,
})));
});
splitMessages = await filterSyncedMessages(splitMessages);
// no messages to add
if (splitMessages.length === 0) {
return { count: 0 };
}
const transformedMessages = splitMessages.map((m) => ({
id: m.id,
role: m.is_user ? 'user' : 'assistant',
content: m.mes,
date: m.send_date,
meta: JSON.stringify(m),
}));
const addMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id, messages: transformedMessages }),
});
if (addMessagesResult.ok) {
const addMessagesData = await addMessagesResult.json();
return addMessagesData; // { count: 1 }
}
return { count: 0 };
}
async function filterSyncedMessages(splitMessages) {
const syncState = await getChatSyncState();
const removeIndices = [];
const syncedIndices = [];
for (let i = 0; i < splitMessages.length; i++) {
const index = splitMessages[i].index;
if (syncState[index]) {
removeIndices.push(i);
continue;
}
syncedIndices.push(index);
}
for (const index of syncedIndices) {
syncState[index] = 1;
}
console.debug('CHROMADB: sync state', syncState.map((v, i) => ({ id: i, synced: v })));
await dbStore.setItem(getCurrentChatId(), syncState);
// remove messages that are already synced
return splitMessages.filter((_, i) => !removeIndices.includes(i));
}
async function onPurgeClick() {
const chat_id = getCurrentChatId();
if (!checkChatId(chat_id)) {
return;
}
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/purge';
const purgeResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id }),
});
if (purgeResult.ok) {
await dbStore.removeItem(chat_id);
toastr.success('ChromaDB context has been successfully cleared');
}
}
async function onExportClick() {
const currentChatId = getCurrentChatId();
if (!checkChatId(currentChatId)) {
return;
}
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/export';
const exportResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id: currentChatId }),
});
if (exportResult.ok) {
const data = await exportResult.json();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const href = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.download = currentChatId + '.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
//Show the error from the result without the html, only what's in the body paragraph
let parser = new DOMParser();
let error = await exportResult.text();
let doc = parser.parseFromString(error, 'text/html');
let errorMessage = doc.querySelector('p').textContent;
toastr.error(`An error occurred while attempting to download the data from ChromaDB: ${errorMessage}`);
}
}
function tinyhash(text) {
let hash = 0;
for (let i = 0; i < text.length; ++i) {
hash = ((hash<<5) - hash) + text.charCodeAt(i);
hash = hash & hash; // Keeps it 32-bit allegedly.
}
return hash;
}
async function onSelectImportFile(e) {
const file = e.target.files[0];
const currentChatId = getCurrentChatId();
if (!checkChatId(currentChatId)) {
return;
}
if (!file) {
return;
}
try {
toastr.info('This may take some time, depending on the file size', 'Processing...');
const text = await getFileText(file);
const imported = JSON.parse(text);
const id_salt = "-" + tinyhash(imported.chat_id).toString(36);
for (let entry of imported.content) {
entry.id = entry.id + id_salt;
}
imported.chat_id = currentChatId;
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/import';
const importResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify(imported),
});
if (importResult.ok) {
const importResultData = await importResult.json();
toastr.success(`Number of chunks: ${importResultData.count}`, 'Injected successfully!');
return importResultData;
} else {
throw new Error();
}
}
catch (error) {
console.log(error);
toastr.error('Something went wrong while importing the data');
}
finally {
e.target.form.reset();
}
}
async function queryMessages(chat_id, query) {
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/query';
const queryMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id, query, n_results: extension_settings.chromadb.n_results }),
});
if (queryMessagesResult.ok) {
const queryMessagesData = await queryMessagesResult.json();
return queryMessagesData;
}
return [];
}
async function queryMultiMessages(chat_id, query) {
const context = getContext();
const response = await fetch("/getallchatsofcharacter", {
method: 'POST',
body: JSON.stringify({ avatar_url: context.characters[context.characterId].avatar }),
headers: getRequestHeaders(),
});
if (!response.ok) {
return;
}
let data = await response.json();
data = Object.values(data);
let chat_list = data.sort((a, b) => a["file_name"].localeCompare(b["file_name"])).reverse();
// Extracting chat_ids from the chat_list
chat_list = chat_list.map(chat => chat.file_name.replace(/\.[^/.]+$/, ""));
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/multiquery';
const queryMessagesResult = await fetch(url, {
method: 'POST',
body: JSON.stringify({ chat_list, query, n_results: extension_settings.chromadb.n_results }),
headers: postHeaders,
});
if (queryMessagesResult.ok) {
const queryMessagesData = await queryMessagesResult.json();
return queryMessagesData;
}
return [];
}
async function onSelectInjectFile(e) {
const file = e.target.files[0];
const currentChatId = getCurrentChatId();
if (!checkChatId(currentChatId)) {
return;
}
if (!file) {
return;
}
try {
toastr.info('This may take some time, depending on the file size', 'Processing...');
const text = await getFileText(file);
extension_settings.chromadb.file_split_type = "newline";
//allow splitting on newlines or splitrecursively
let split = [];
if (extension_settings.chromadb.file_split_type == "newline") {
split = text.split(/\r?\n/).filter(onlyUnique);
} else {
split = splitRecursive(text, extension_settings.chromadb.file_split_length).filter(onlyUnique);
}
const baseDate = Date.now();
const messages = split.map((m, i) => ({
id: `${file.name}-${split.indexOf(m)}`,
role: 'system',
content: m,
date: baseDate + i,
meta: JSON.stringify({
name: file.name,
is_user: false,
is_system: false,
send_date: humanizedDateTime(),
mes: m,
extra: {
type: system_message_types.NARRATOR,
}
}),
}));
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb';
const addMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id: currentChatId, messages: messages }),
});
if (addMessagesResult.ok) {
const addMessagesData = await addMessagesResult.json();
toastr.success(`Number of chunks: ${addMessagesData.count}`, 'Injected successfully!');
return addMessagesData;
} else {
throw new Error();
}
}
catch (error) {
console.log(error);
toastr.error('Something went wrong while injecting the data');
}
finally {
e.target.form.reset();
}
}
// Gets the length of character description in the current context
function getCharacterDataLength() {
const context = getContext();
const character = context.characters[context.characterId];
if (typeof character?.data !== 'object') {
return 0;
}
let characterDataLength = 0;
for (const [key, value] of Object.entries(character.data)) {
if (typeof value !== 'string') {
continue;
}
if (['description', 'personality', 'scenario'].includes(key)) {
characterDataLength += character.data[key].length;
}
}
return characterDataLength;
}
/*
* Automatically adjusts the extension settings for the optimal number of messages to keep and query based
* on the chat history and a specified maximum context length.
*/
function doAutoAdjust(chat, maxContext) {
// Only valid for chat injections strategy
if (extension_settings.chromadb.recall_strategy !== 0) {
return;
}
console.debug('CHROMADB: Auto-adjusting sliders (messages: %o, maxContext: %o)', chat.length, maxContext);
// Get mean message length
const meanMessageLength = chat.reduce((acc, cur) => acc + (cur?.mes?.length ?? 0), 0) / chat.length;
if (Number.isNaN(meanMessageLength) || meanMessageLength === 0) {
console.debug('CHROMADB: Mean message length is zero or NaN, aborting auto-adjust');
return;
}
// Adjust max context for character defs length
maxContext = Math.floor(maxContext - (getCharacterDataLength() / CHARACTERS_PER_TOKEN_RATIO));
console.debug('CHROMADB: Max context adjusted for character defs: %o', maxContext);
console.debug('CHROMADB: Mean message length (characters): %o', meanMessageLength);
// Convert to number of "tokens"
const meanMessageLengthTokens = Math.ceil(meanMessageLength / CHARACTERS_PER_TOKEN_RATIO);
console.debug('CHROMADB: Mean message length (tokens): %o', meanMessageLengthTokens);
// Get number of messages in context
const contextMessages = Math.max(1, Math.ceil(maxContext / meanMessageLengthTokens));
// Round up to nearest 5
const contextMessagesRounded = Math.ceil(contextMessages / 5) * 5;
console.debug('CHROMADB: Estimated context messages (rounded): %o', contextMessagesRounded);
// Messages to keep (proportional, rounded to nearest 5, minimum 5, maximum 500)
const messagesToKeep = Math.min(defaultSettings.keep_context_max, Math.max(5, Math.floor(contextMessagesRounded * extension_settings.chromadb.keep_context_proportion / 5) * 5));
console.debug('CHROMADB: Estimated messages to keep: %o', messagesToKeep);
// Messages to query (rounded, maximum 500)
const messagesToQuery = Math.min(defaultSettings.n_results_max, contextMessagesRounded - messagesToKeep);
console.debug('CHROMADB: Estimated messages to query: %o', messagesToQuery);
// Set extension settings
extension_settings.chromadb.keep_context = messagesToKeep;
extension_settings.chromadb.n_results = messagesToQuery;
// Update sliders
$('#chromadb_keep_context').val(messagesToKeep);
$('#chromadb_n_results').val(messagesToQuery);
// Update labels
$('#chromadb_keep_context_value').text(extension_settings.chromadb.keep_context);
$('#chromadb_n_results_value').text(extension_settings.chromadb.n_results);
}
window.chromadb_interceptGeneration = async (chat, maxContext) => {
if (extension_settings.chromadb.auto_adjust) {
doAutoAdjust(chat, maxContext);
}
const currentChatId = getCurrentChatId();
if (!currentChatId)
return;
//log the current settings
console.debug("CHROMADB: Current settings: %o", extension_settings.chromadb);
const selectedStrategy = extension_settings.chromadb.strategy;
const recallStrategy = extension_settings.chromadb.recall_strategy;
let recallMsg = extension_settings.chromadb.recall_msg || defaultSettings.chroma_default_msg;
const chromaDepth = extension_settings.chromadb.chroma_depth;
const chromaSortStrategy = extension_settings.chromadb.sort_strategy;
const chromaQueryLastOnly = extension_settings.chromadb.query_last_only;
const messagesToStore = chat.slice(0, -extension_settings.chromadb.keep_context);
if (messagesToStore.length > 0 && !extension_settings.chromadb.freeze) {
//log the messages to store
console.debug("CHROMADB: Messages to store: %o", messagesToStore);
//log the messages to store length vs keep context
console.debug("CHROMADB: Messages to store length vs keep context: %o vs %o", messagesToStore.length, extension_settings.chromadb.keep_context);
await addMessages(currentChatId, messagesToStore);
}
const lastMessage = chat[chat.length - 1];
let queriedMessages;
if (lastMessage) {
let queryBlob = "";
if (chromaQueryLastOnly) {
queryBlob = lastMessage.mes;
}
else {
for (let msg of chat.slice(-extension_settings.chromadb.keep_context)) {
queryBlob += `${msg.mes}\n`
}
}
console.debug("CHROMADB: Query text:", queryBlob);
if (recallStrategy === 'multichat') {
console.log("Utilizing multichat")
queriedMessages = await queryMultiMessages(currentChatId, queryBlob);
}
else {
queriedMessages = await queryMessages(currentChatId, queryBlob);
}
if (chromaSortStrategy === "date") {
queriedMessages.sort((a, b) => a.date - b.date);
}
else {
queriedMessages.sort((a, b) => b.distance - a.distance);
}
console.debug("CHROMADB: Query results: %o", queriedMessages);
let newChat = [];
if (selectedStrategy === 'ross') {
//adds chroma to the end of chat and allows Generate() to cull old messages naturally.
const context = getContext();
const charname = context.name2;
newChat.push(
{
is_user: false,
mes: `[Use these past chat exchanges to inform ${charname}'s next response:`,
name: "system",
send_date: 0,
}
);
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
newChat.push(
{
is_user: false,
mes: `]\n`,
name: "system",
send_date: 0,
}
);
chat.splice(chat.length, 0, ...newChat);
}
if (selectedStrategy === 'hh_aa') {
// Insert chroma history messages as a list at the AFTER_SCENARIO anchor point
const context = getContext();
const chromaTokenLimit = extension_settings.chromadb.hhaa_token_limit;
let wrapperMsg = extension_settings.chromadb.hhaa_wrapper_msg || defaultSettings.chroma_default_hhaa_wrapper;
wrapperMsg = substituteParams(wrapperMsg, context.name1, context.name2);
if (!wrapperMsg.includes("{{memories}}")) {
wrapperMsg += " {{memories}}";
}
let memoryMsg = extension_settings.chromadb.hhaa_memory_msg || defaultSettings.chroma_default_hhaa_memory;
memoryMsg = substituteParams(memoryMsg, context.name1, context.name2);
if (!memoryMsg.includes("{{message}}")) {
memoryMsg += " {{message}}";
}
// Reversed because we want the most 'important' messages at the bottom.
let recalledMemories = queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse).reverse();
let tokenApprox = 0;
let allMemoryBlob = "";
let seenMemories = new Set(); // Why are there even duplicates in chromadb anyway?
for (const msg of recalledMemories) {
const memoryBlob = memoryMsg.replace('{{name}}', msg.name).replace('{{message}}', msg.mes);
const memoryTokens = (memoryBlob.length / CHARACTERS_PER_TOKEN_RATIO);
if (!seenMemories.has(memoryBlob) && tokenApprox + memoryTokens <= chromaTokenLimit) {
allMemoryBlob += memoryBlob;
tokenApprox += memoryTokens;
seenMemories.add(memoryBlob);
}
}
// No memories? No prompt.
const promptBlob = (tokenApprox == 0) ? "" : wrapperMsg.replace('{{memories}}', allMemoryBlob);
console.debug("CHROMADB: prompt blob: %o", promptBlob);
context.setExtensionPrompt(MODULE_NAME, promptBlob, extension_prompt_types.IN_PROMPT);
}
if (selectedStrategy === 'custom') {
const context = getContext();
recallMsg = substituteParams(recallMsg, context.name1, context.name2);
if (!recallMsg.includes("{{memories}}")) {
recallMsg += " {{memories}}";
}
let recallStart = recallMsg.split('{{memories}}')[0]
let recallEnd = recallMsg.split('{{memories}}')[1]
newChat.push(
{
is_user: false,
mes: recallStart,
name: "system",
send_date: 0,
}
);
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
newChat.push(
{
is_user: false,
mes: recallEnd + `\n`,
name: "system",
send_date: 0,
}
);
//prototype chroma duplicate removal
let chatset = new Set(chat.map(obj => obj.mes));
newChat = newChat.filter(obj => !chatset.has(obj.mes));
if(chromaDepth === -1) {
chat.splice(chat.length, 0, ...newChat);
}
else {
chat.splice(chromaDepth, 0, ...newChat);
}
}
if (selectedStrategy === 'original') {
//removes .length # messages from the start of 'kept messages'
//replaces them with chromaDB results (with no separator)
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
chat.splice(0, messagesToStore.length, ...newChat);
}
}
}
function onFreezeInput() {
extension_settings.chromadb.freeze = $('#chromadb_freeze').is(':checked');
saveSettingsDebounced();
}
function onAutoAdjustInput() {
extension_settings.chromadb.auto_adjust = $('#chromadb_auto_adjust').is(':checked');
enableDisableSliders();
saveSettingsDebounced();
}
function onFullLogQuery() {
extension_settings.chromadb.query_last_only = $('#chromadb_query_last_only').is(':checked');
saveSettingsDebounced();
}
function enableDisableSliders() {
const auto_adjust = extension_settings.chromadb.auto_adjust;
$('label[for="chromadb_keep_context"]').prop('hidden', auto_adjust);
$('#chromadb_keep_context').prop('hidden', auto_adjust)
$('label[for="chromadb_n_results"]').prop('hidden', auto_adjust);
$('#chromadb_n_results').prop('hidden', auto_adjust)
$('label[for="chromadb_keep_context_proportion"]').prop('hidden', !auto_adjust);
$('#chromadb_keep_context_proportion').prop('hidden', !auto_adjust)
}
function onKeepContextProportionInput() {
extension_settings.chromadb.keep_context_proportion = $('#chromadb_keep_context_proportion').val();
$('#chromadb_keep_context_proportion_value').text(Math.round(extension_settings.chromadb.keep_context_proportion * 100));
saveSettingsDebounced();
}
jQuery(async () => {
const settingsHtml = `
<div class="chromadb_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Smart Context</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</small>
<span class="wide100p marginTopBot5 displayBlock">Memory Injection Strategy</span>
<hr>
<select id="chromadb_strategy">
<option value="original">Replace non-kept chat items with memories</option>
<option value="ross">Add memories after chat with a header tag</option>
<option value="hh_aa">Add memory list to character description</option>
<option value="custom">Add memories at custom depth with custom msg</option>
</select>
<label for="chromadb_custom_msg" hidden><small>Custom injection message:</small></label>
<textarea id="chromadb_custom_msg" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_msg}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_custom_depth" hidden><small>How deep should the memory messages be injected?: (<span id="chromadb_custom_depth_value"></span>)</small></label>
<input id="chromadb_custom_depth" type="range" min="${defaultSettings.chroma_depth_min}" max="${defaultSettings.chroma_depth_max}" step="${defaultSettings.chroma_depth_step}" value="${defaultSettings.chroma_depth}" hidden/>
<label for="chromadb_hhaa_wrapperfmt" hidden><small>Custom wrapper format:</small></label>
<textarea id="chromadb_hhaa_wrapperfmt" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_hhaa_wrapper}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_hhaa_memoryfmt" hidden><small>Custom memory format:</small></label>
<textarea id="chromadb_hhaa_memoryfmt" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_hhaa_memory}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_hhaa_token_limit" hidden><small>Maximum tokens allowed for memories: (<span id="chromadb_hhaa_token_limit_value"></span>)</small></label>
<input id="chromadb_hhaa_token_limit" type="range" min="0" max="2048" step="64" value="${defaultSettings.hhaa_token_limit}" hidden/>
<span>Memory Recall Strategy</span>
<select id="chromadb_recall_strategy">
<option value="original">Recall only from this chat</option>
<option value="multichat">Recall from all character chats (experimental)</option>
</select>
<span>Memory Sort Strategy</span>
<select id="chromadb_sort_strategy">
<option value="date">Sort memories by date</option>
<option value="distance">Sort memories by relevance</option>
</select>
<label for="chromadb_keep_context"><small>How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</small></label>
<input id="chromadb_keep_context" type="range" min="${defaultSettings.keep_context_min}" max="${defaultSettings.keep_context_max}" step="${defaultSettings.keep_context_step}" value="${defaultSettings.keep_context}" />
<label for="chromadb_n_results"><small>Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</small></label>
<input id="chromadb_n_results" type="range" min="${defaultSettings.n_results_min}" max="${defaultSettings.n_results_max}" step="${defaultSettings.n_results_step}" value="${defaultSettings.n_results}" />
<label for="chromadb_keep_context_proportion"><small>Keep (<span id="chromadb_keep_context_proportion_value"></span>%) of in-context chat messages; replace the rest with memories</small></label>
<input id="chromadb_keep_context_proportion" type="range" min="${defaultSettings.keep_context_proportion_min}" max="${defaultSettings.keep_context_proportion_max}" step="${defaultSettings.keep_context_proportion_step}" value="${defaultSettings.keep_context_proportion}" />
<label for="chromadb_split_length"><small>Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</small></label>
<input id="chromadb_split_length" type="range" min="${defaultSettings.split_length_min}" max="${defaultSettings.split_length_max}" step="${defaultSettings.split_length_step}" value="${defaultSettings.split_length}" />
<label for="chromadb_file_split_length"><small>Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</small></label>
<input id="chromadb_file_split_length" type="range" min="${defaultSettings.file_split_length_min}" max="${defaultSettings.file_split_length_max}" step="${defaultSettings.file_split_length_step}" value="${defaultSettings.file_split_length}" />
<label class="checkbox_label" for="chromadb_freeze" title="Pauses the automatic synchronization of new messages with ChromaDB. Older messages and injections will still be pulled as usual." >
<input type="checkbox" id="chromadb_freeze" />
<span>Freeze ChromaDB state</span>
</label>
<label class="checkbox_label for="chromadb_auto_adjust" title="Automatically adjusts the number of messages to keep based on the average number of messages in the current chat and the chosen proportion.">
<input type="checkbox" id="chromadb_auto_adjust" />
<span>Use % strategy</span>
</label>
<label class="checkbox_label" for="chromadb_chunk_nl" title="Chunk injected documents on newline instead of at set character size." >
<input type="checkbox" id="chromadb_chunk_nl" />
<span>Chunk on Newlines</span>
</label>
<label class="checkbox_label for="chromadb_query_last_only" title="ChromaDB queries only use the most recent message. (Instead of using all messages still in the context.)">
<input type="checkbox" id="chromadb_query_last_only" />
<span>Query last message only</span>
</label>
<div class="flex-container spaceEvenly">
<div id="chromadb_inject" title="Upload custom textual data to use in the context of the current chat" class="menu_button">
<i class="fa-solid fa-file-arrow-up"></i>
<span>Inject Data (TXT file)</span>
</div>
<div id="chromadb_export" title="Export all of the current chromadb data for this current chat" class="menu_button">
<i class="fa-solid fa-file-export"></i>
<span>Export</span>
</div>
<div id="chromadb_import" title="Import a full chromadb export for this current chat" class="menu_button">
<i class="fa-solid fa-file-import"></i>
<span>Import</span>
</div>
<div id="chromadb_purge" title="Force purge all the data related to the current chat from the database" class="menu_button">
<i class="fa-solid fa-broom"></i>
<span>Purge Chat from the DB</span>
</div>
</div>
<small><i>Local ChromaDB now persists to disk by default. The default folder is .chroma_db, and you can set a different folder with the --chroma-folder argument. If you are using the Extras Colab notebook, you will need to inject the text data every time the Extras API server is restarted.</i></small>
</div>
<form><input id="chromadb_inject_file" type="file" accept="text/plain" hidden></form>
<form><input id="chromadb_import_file" type="file" accept="application/json" hidden></form>
</div>`;
$('#extensions_settings2').append(settingsHtml);
$('#chromadb_strategy').on('change', onStrategyChange);
$('#chromadb_recall_strategy').on('change', onRecallStrategyChange);
$('#chromadb_sort_strategy').on('change', onSortStrategyChange);
$('#chromadb_keep_context').on('input', onKeepContextInput);
$('#chromadb_n_results').on('input', onNResultsInput);
$('#chromadb_custom_depth').on('input', onChromaDepthInput);
$('#chromadb_custom_msg').on('input', onChromaMsgInput);
$('#chromadb_hhaa_wrapperfmt').on('input', onChromaHHAAWrapper);
$('#chromadb_hhaa_memoryfmt').on('input', onChromaHHAAMemory);
$('#chromadb_hhaa_token_limit').on('input', onChromaHHAATokens);
$('#chromadb_split_length').on('input', onSplitLengthInput);
$('#chromadb_file_split_length').on('input', onFileSplitLengthInput);
$('#chromadb_inject').on('click', () => $('#chromadb_inject_file').trigger('click'));
$('#chromadb_import').on('click', () => $('#chromadb_import_file').trigger('click'));
$('#chromadb_inject_file').on('change', onSelectInjectFile);
$('#chromadb_import_file').on('change', onSelectImportFile);
$('#chromadb_purge').on('click', onPurgeClick);
$('#chromadb_export').on('click', onExportClick);
$('#chromadb_freeze').on('input', onFreezeInput);
$('#chromadb_chunk_nl').on('input', onChunkNLInput);
$('#chromadb_auto_adjust').on('input', onAutoAdjustInput);
$('#chromadb_query_last_only').on('input', onFullLogQuery);
$('#chromadb_keep_context_proportion').on('input', onKeepContextProportionInput);
await loadSettings();
// Not sure if this is needed, but it's here just in case
eventSource.on(event_types.MESSAGE_DELETED, getChatSyncState);
eventSource.on(event_types.MESSAGE_RECEIVED, getChatSyncState);
eventSource.on(event_types.MESSAGE_SENT, getChatSyncState);
// Will make the sync state update when a message is edited or swiped
eventSource.on(event_types.MESSAGE_EDITED, invalidateMessageSyncState);
eventSource.on(event_types.MESSAGE_SWIPED, invalidateMessageSyncState);
});

View File

@ -1,14 +0,0 @@
{
"display_name": "Smart Context",
"loading_order": 11,
"requires": [
"chromadb"
],
"optional": [],
"generate_interceptor": "chromadb_interceptGeneration",
"js": "index.js",
"css": "style.css",
"author": "maceter636@proton.me",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,7 +0,0 @@
.chromadb_settings .menu_button {
width: fit-content;
display: flex;
gap: 10px;
align-items: center;
flex-direction: row;
}

View File

@ -3,6 +3,8 @@ import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } fro
import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from "../../../script.js"; import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from "../../../script.js";
import { is_group_generating, selected_group } from "../../group-chats.js"; import { is_group_generating, selected_group } from "../../group-chats.js";
import { registerSlashCommand } from "../../slash-commands.js"; import { registerSlashCommand } from "../../slash-commands.js";
import { loadMovingUIState } from '../../power-user.js';
import { dragElement } from "../../RossAscends-mods.js";
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = '1_memory'; const MODULE_NAME = '1_memory';
@ -61,6 +63,7 @@ const defaultSettings = {
maxLengthPenalty: 4, maxLengthPenalty: 4,
lengthPenaltyStep: 0.1, lengthPenaltyStep: 0.1,
memoryFrozen: false, memoryFrozen: false,
SkipWIAN: false,
source: summary_sources.extras, source: summary_sources.extras,
prompt: defaultPrompt, prompt: defaultPrompt,
template: defaultTemplate, template: defaultTemplate,
@ -98,6 +101,7 @@ function loadSettings() {
$('#memory_temperature').val(extension_settings.memory.temperature).trigger('input'); $('#memory_temperature').val(extension_settings.memory.temperature).trigger('input');
$('#memory_length_penalty').val(extension_settings.memory.lengthPenalty).trigger('input'); $('#memory_length_penalty').val(extension_settings.memory.lengthPenalty).trigger('input');
$('#memory_frozen').prop('checked', extension_settings.memory.memoryFrozen).trigger('input'); $('#memory_frozen').prop('checked', extension_settings.memory.memoryFrozen).trigger('input');
$('#memory_skipWIAN').prop('checked', extension_settings.memory.SkipWIAN).trigger('input');
$('#memory_prompt').val(extension_settings.memory.prompt).trigger('input'); $('#memory_prompt').val(extension_settings.memory.prompt).trigger('input');
$('#memory_prompt_words').val(extension_settings.memory.promptWords).trigger('input'); $('#memory_prompt_words').val(extension_settings.memory.promptWords).trigger('input');
$('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input'); $('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input');
@ -168,6 +172,12 @@ function onMemoryFrozenInput() {
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onMemorySkipWIANInput() {
const value = Boolean($(this).prop('checked'));
extension_settings.memory.SkipWIAN = value;
saveSettingsDebounced();
}
function onMemoryPromptWordsInput() { function onMemoryPromptWordsInput() {
const value = $(this).val(); const value = $(this).val();
extension_settings.memory.promptWords = Number(value); extension_settings.memory.promptWords = Number(value);
@ -309,13 +319,15 @@ async function onChatEvent() {
async function forceSummarizeChat() { async function forceSummarizeChat() {
const context = getContext(); const context = getContext();
const skipWIAN = extension_settings.memory.SkipWIAN
console.log(`Skipping WIAN? ${skipWIAN}`)
if (!context.chatId) { if (!context.chatId) {
toastr.warning('No chat selected'); toastr.warning('No chat selected');
return; return;
} }
toastr.info('Summarizing chat...', 'Please wait'); toastr.info('Summarizing chat...', 'Please wait');
const value = await summarizeChatMain(context, true); const value = await summarizeChatMain(context, true, skipWIAN);
if (!value) { if (!value) {
toastr.warning('Failed to summarize chat'); toastr.warning('Failed to summarize chat');
@ -324,19 +336,21 @@ async function forceSummarizeChat() {
} }
async function summarizeChat(context) { async function summarizeChat(context) {
const skipWIAN = extension_settings.memory.SkipWIAN
switch (extension_settings.memory.source) { switch (extension_settings.memory.source) {
case summary_sources.extras: case summary_sources.extras:
await summarizeChatExtras(context); await summarizeChatExtras(context);
break; break;
case summary_sources.main: case summary_sources.main:
await summarizeChatMain(context, false); await summarizeChatMain(context, false, skipWIAN);
break; break;
default: default:
break; break;
} }
} }
async function summarizeChatMain(context, force) { async function summarizeChatMain(context, force, skipWIAN) {
if (extension_settings.memory.promptInterval === 0 && !force) { if (extension_settings.memory.promptInterval === 0 && !force) {
console.debug('Prompt interval is set to 0, skipping summarization'); console.debug('Prompt interval is set to 0, skipping summarization');
return; return;
@ -395,8 +409,8 @@ async function summarizeChatMain(context, force) {
console.debug('Summarization prompt is empty. Skipping summarization.'); console.debug('Summarization prompt is empty. Skipping summarization.');
return; return;
} }
console.log('sending summary prompt')
const summary = await generateQuietPrompt(prompt, false); const summary = await generateQuietPrompt(prompt, false, skipWIAN);
const newContext = getContext(); const newContext = getContext();
// something changed during summarization request // something changed during summarization request
@ -547,100 +561,178 @@ function setMemoryContext(value, saveToMessage) {
} }
} }
function doPopout(e) {
const target = e.target;
//repurposes the zoomed avatar template to server as a floating div
if ($("#summaryExtensionPopout").length === 0) {
console.debug('did not see popout yet, creating')
const originalHTMLClone = $(target).parent().parent().parent().find('.inline-drawer-content').html()
const originalElement = $(target).parent().parent().parent().find('.inline-drawer-content')
const template = $('#zoomed_avatar_template').html();
const controlBarHtml = `<div class="panelControlBar flex-container">
<div id="summaryExtensionPopoutheader" class="fa-solid fa-grip drag-grabber hoverglow"></div>
<div id="summaryExtensionPopoutClose" class="fa-solid fa-circle-xmark hoverglow dragClose"></div>
</div>`
const newElement = $(template);
newElement.attr('id', 'summaryExtensionPopout')
.removeClass('zoomed_avatar')
.addClass('draggable')
.empty()
const prevSummaryBoxContents = $('#memory_contents').val(); //copy summary box before emptying
originalElement.empty();
originalElement.html(`<div class="flex-container alignitemscenter justifyCenter wide100p"><small>Currently popped out</small></div>`)
newElement.append(controlBarHtml).append(originalHTMLClone)
$('body').append(newElement);
$("#summaryExtensionDrawerContents").addClass('scrollableInnerFull')
setMemoryContext(prevSummaryBoxContents, false); //paste prev summary box contents into popout box
setupListeners();
loadSettings();
loadMovingUIState();
$("#summaryExtensionPopout").fadeIn(250);
dragElement(newElement);
//setup listener for close button to restore extensions menu
$('#summaryExtensionPopoutClose').off('click').on('click', function () {
$("#summaryExtensionDrawerContents").removeClass('scrollableInnerFull')
const summaryPopoutHTML = $("#summaryExtensionDrawerContents")
$("#summaryExtensionPopout").fadeOut(250, () => {
originalElement.empty();
originalElement.html(summaryPopoutHTML);
$("#summaryExtensionPopout").remove()
})
loadSettings();
})
} else {
console.debug('saw existing popout, removing')
$("#summaryExtensionPopout").fadeOut(250, () => { $("#summaryExtensionPopoutClose").trigger('click') });
}
}
function setupListeners() {
//setup shared listeners for popout and regular ext menu
$('#memory_restore').off('click').on('click', onMemoryRestoreClick);
$('#memory_contents').off('click').on('input', onMemoryContentInput);
$('#memory_long_length').off('click').on('input', onMemoryLongInput);
$('#memory_short_length').off('click').on('input', onMemoryShortInput);
$('#memory_repetition_penalty').off('click').on('input', onMemoryRepetitionPenaltyInput);
$('#memory_temperature').off('click').on('input', onMemoryTemperatureInput);
$('#memory_length_penalty').off('click').on('input', onMemoryLengthPenaltyInput);
$('#memory_frozen').off('click').on('input', onMemoryFrozenInput);
$('#memory_skipWIAN').off('click').on('input', onMemorySkipWIANInput);
$('#summary_source').off('click').on('change', onSummarySourceChange);
$('#memory_prompt_words').off('click').on('input', onMemoryPromptWordsInput);
$('#memory_prompt_interval').off('click').on('input', onMemoryPromptIntervalInput);
$('#memory_prompt').off('click').on('input', onMemoryPromptInput);
$('#memory_force_summarize').off('click').on('click', forceSummarizeChat);
$('#memory_template').off('click').on('input', onMemoryTemplateInput);
$('#memory_depth').off('click').on('input', onMemoryDepthInput);
$('input[name="memory_position"]').off('click').on('change', onMemoryPositionChange);
$('#memory_prompt_words_force').off('click').on('input', onMemoryPromptWordsForceInput);
$("#summarySettingsBlockToggle").off('click').on('click', function () {
console.log('saw settings button click')
$("#summarySettingsBlock").slideToggle(200, "swing"); //toggleClass("hidden");
});
}
jQuery(function () { jQuery(function () {
function addExtensionControls() { function addExtensionControls() {
const settingsHtml = ` const settingsHtml = `
<div id="memory_settings"> <div id="memory_settings">
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<b>Summarize</b> <div class="flex-container alignitemscenter margin0"><b>Summarize</b><i id="summaryExtensionPopoutButton" class="fa-solid fa-window-restore menu_button margin0"></i></div>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<label for="summary_source">Summarization source:</label> <div id="summaryExtensionDrawerContents">
<select id="summary_source"> <label for="summary_source">Summarize with:</label>
<option value="main">Main API</option> <select id="summary_source">
<option value="extras">Extras API</option> <option value="main">Main API</option>
</select> <option value="extras">Extras API</option>
<label for="memory_contents">Current summary: </label> </select><br>
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls"> <div class="flex-container justifyspacebetween alignitemscenter">
<input id="memory_restore" class="menu_button" type="button" value="Restore previous state" />
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Pause summarization</label> <span class="flex1">Current summary:</span>
</div> <div id="memory_restore" class="menu_button flex1 margin0"><span>Restore Previous</span></div>
<div class="memory_template">
<label for="memory_template">Injection template:</label> </div>
<textarea id="memory_template" class="text_pole textarea_compact" rows="1" placeholder="Use {{summary}} macro to specify the position of summarized text."></textarea>
</div> <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<label for="memory_position">Injection position:</label> <div class="memory_contents_controls">
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="2" />
Before Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="0" />
After Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="1" />
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
</label>
</div>
<div data-source="main" class="memory_contents_controls">
</div>
<div data-source="main">
<label for="memory_prompt" class="title_restorable">
Summarization Prompt
<div id="memory_force_summarize" class="menu_button menu_button_icon"> <div id="memory_force_summarize" class="menu_button menu_button_icon">
<i class="fa-solid fa-database"></i> <i class="fa-solid fa-database"></i>
<span>Generate now</span> <span>Summarize now</span>
</div> </div>
</label> <label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Pause</label>
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be used in summary generation. Insert {{words}} macro to use the "Number of words" parameter."></textarea> <label for="memory_skipWIAN"><input id="memory_skipWIAN" type="checkbox" />No WI/AN</label>
<label for="memory_prompt_words">Number of words in the summary (<span id="memory_prompt_words_value"></span> words)</label> </div>
<input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" /> <div class="memory_contents_controls">
<label for="memory_prompt_interval">Update interval (<span id="memory_prompt_interval_value"></span> messages)</label> <div id="summarySettingsBlockToggle" class="menu_button">Settings</div>
<small>Set to 0 to disable</small> </div>
<input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" /> <div id="summarySettingsBlock" style="display:none;">
<label for="memory_prompt_words_force">Force update after (<span id="memory_prompt_words_force_value"></span> words)</label> <div class="memory_template">
<small>Set to 0 to disable</small> <label for="memory_template">Insertion string:</label>
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" /> <textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary}} will resolve to the current summary contents."></textarea>
</div> </div>
<div data-source="extras"> <label for="memory_position">Position:</label>
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label> <div class="radio_group">
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" /> <label>
<label for="memory_long_length">Summary output length (<span id="memory_long_length_tokens"></span> tokens)</label> <input type="radio" name="memory_position" value="2" />
<input id="memory_long_length" type="range" value="${defaultSettings.longMemoryLength}" min="${defaultSettings.minLongMemory}" max="${defaultSettings.maxLongMemory}" step="${defaultSettings.longMemoryStep}" /> Before Main Prompt / Story String
<label for="memory_temperature">Temperature (<span id="memory_temperature_value"></span>)</label> </label>
<input id="memory_temperature" type="range" value="${defaultSettings.temperature}" min="${defaultSettings.minTemperature}" max="${defaultSettings.maxTemperature}" step="${defaultSettings.temperatureStep}" /> <label>
<label for="memory_repetition_penalty">Repetition penalty (<span id="memory_repetition_penalty_value"></span>)</label> <input type="radio" name="memory_position" value="0" />
<input id="memory_repetition_penalty" type="range" value="${defaultSettings.repetitionPenalty}" min="${defaultSettings.minRepetitionPenalty}" max="${defaultSettings.maxRepetitionPenalty}" step="${defaultSettings.repetitionPenaltyStep}" /> After Main Prompt / Story String
<label for="memory_length_penalty">Length preference <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label> </label>
<input id="memory_length_penalty" type="range" value="${defaultSettings.lengthPenalty}" min="${defaultSettings.minLengthPenalty}" max="${defaultSettings.maxLengthPenalty}" step="${defaultSettings.lengthPenaltyStep}" /> <label>
<input type="radio" name="memory_position" value="1" />
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
</label>
</div>
<div data-source="main" class="memory_contents_controls">
</div>
<div data-source="main">
<label for="memory_prompt" class="title_restorable">
Summary Prompt
</label>
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} will resolve to the 'Number of words' parameter."></textarea>
<label for="memory_prompt_words">Summary length (<span id="memory_prompt_words_value"></span> words)</label>
<input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" />
<label for="memory_prompt_interval">Update every <span id="memory_prompt_interval_value"></span> messages</label>
<small>0 = disable</small>
<input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" />
<label for="memory_prompt_words_force">Update every <span id="memory_prompt_words_force_value"></span> words</label>
<small>0 = disable</small>
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" />
<small>If both sliders are non-zero, then both will trigger summary updates a their respective intervals.</small>
</div>
<div data-source="extras">
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" />
<label for="memory_long_length">Summary output length (<span id="memory_long_length_tokens"></span> tokens)</label>
<input id="memory_long_length" type="range" value="${defaultSettings.longMemoryLength}" min="${defaultSettings.minLongMemory}" max="${defaultSettings.maxLongMemory}" step="${defaultSettings.longMemoryStep}" />
<label for="memory_temperature">Temperature (<span id="memory_temperature_value"></span>)</label>
<input id="memory_temperature" type="range" value="${defaultSettings.temperature}" min="${defaultSettings.minTemperature}" max="${defaultSettings.maxTemperature}" step="${defaultSettings.temperatureStep}" />
<label for="memory_repetition_penalty">Repetition penalty (<span id="memory_repetition_penalty_value"></span>)</label>
<input id="memory_repetition_penalty" type="range" value="${defaultSettings.repetitionPenalty}" min="${defaultSettings.minRepetitionPenalty}" max="${defaultSettings.maxRepetitionPenalty}" step="${defaultSettings.repetitionPenaltyStep}" />
<label for="memory_length_penalty">Length preference <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
<input id="memory_length_penalty" type="range" value="${defaultSettings.lengthPenalty}" min="${defaultSettings.minLengthPenalty}" max="${defaultSettings.maxLengthPenalty}" step="${defaultSettings.lengthPenaltyStep}" />
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
`; `;
$('#extensions_settings2').append(settingsHtml); $('#extensions_settings2').append(settingsHtml);
$('#memory_restore').on('click', onMemoryRestoreClick); setupListeners();
$('#memory_contents').on('input', onMemoryContentInput); $("#summaryExtensionPopoutButton").off('click').on('click', function (e) {
$('#memory_long_length').on('input', onMemoryLongInput); doPopout(e);
$('#memory_short_length').on('input', onMemoryShortInput); e.stopPropagation();
$('#memory_repetition_penalty').on('input', onMemoryRepetitionPenaltyInput); });
$('#memory_temperature').on('input', onMemoryTemperatureInput);
$('#memory_length_penalty').on('input', onMemoryLengthPenaltyInput);
$('#memory_frozen').on('input', onMemoryFrozenInput);
$('#summary_source').on('change', onSummarySourceChange);
$('#memory_prompt_words').on('input', onMemoryPromptWordsInput);
$('#memory_prompt_interval').on('input', onMemoryPromptIntervalInput);
$('#memory_prompt').on('input', onMemoryPromptInput);
$('#memory_force_summarize').on('click', forceSummarizeChat);
$('#memory_template').on('input', onMemoryTemplateInput);
$('#memory_depth').on('input', onMemoryDepthInput);
$('input[name="memory_position"]').on('change', onMemoryPositionChange);
$('#memory_prompt_words_force').on('input', onMemoryPromptWordsForceInput);
} }
addExtensionControls(); addExtensionControls();

View File

@ -1,5 +1,5 @@
{ {
"display_name": "Memory", "display_name": "Summarize",
"loading_order": 9, "loading_order": 9,
"requires": [], "requires": [],
"optional": [ "optional": [

View File

@ -8,7 +8,8 @@
line-height: 1.2; line-height: 1.2;
} }
label[for="memory_frozen"] { label[for="memory_frozen"],
label[for="memory_skipWIAN"] {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0 !important; margin: 0 !important;
@ -23,4 +24,4 @@ label[for="memory_frozen"] input {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }

View File

@ -1,811 +0,0 @@
import { chat_metadata, callPopup, saveSettingsDebounced, is_send_press } from "../../../script.js";
import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js";
import {
substituteParams,
eventSource,
event_types,
generateQuietPrompt,
} from "../../../script.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { waitUntilCondition } from "../../utils.js";
import { is_group_generating, selected_group } from "../../group-chats.js";
const MODULE_NAME = "Objective"
let taskTree = null
let globalTasks = []
let currentChatId = ""
let currentObjective = null
let currentTask = null
let checkCounter = 0
let lastMessageWasSwipe = false
const defaultPrompts = {
"createTask": `Pause your roleplay. Please generate a numbered list of plain text tasks to complete an objective. The objective that you must make a numbered task list for is: "{{objective}}". The tasks created should take into account the character traits of {{char}}. These tasks may or may not involve {{user}} directly. Include the objective as the final task.`,
"checkTaskCompleted": `Pause your roleplay. Determine if this task is completed: [{{task}}]. To do this, examine the most recent messages. Your response must only contain either true or false, and nothing else. Example output: true`,
'currentTask':`Your current task is [{{task}}]. Balance existing roleplay with completing this task.`,
}
let objectivePrompts = defaultPrompts
//###############################//
//# Task Management #//
//###############################//
// Return the task and index or throw an error
function getTaskById(taskId){
if (taskId == null) {
throw `Null task id`
}
return getTaskByIdRecurse(taskId, taskTree)
}
function getTaskByIdRecurse(taskId, task) {
if (task.id == taskId){
return task
}
for (const childTask of task.children) {
const foundTask = getTaskByIdRecurse(taskId, childTask);
if (foundTask != null) {
return foundTask;
}
}
return null;
}
function substituteParamsPrompts(content, substituteGlobal) {
content = content.replace(/{{objective}}/gi, currentObjective.description)
content = content.replace(/{{task}}/gi, currentTask.description)
if (currentTask.parent){
content = content.replace(/{{parent}}/gi, currentTask.parent.description)
}
if (substituteGlobal) {
content = substituteParams(content)
}
return content
}
// Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much.
async function generateTasks() {
const prompt = substituteParamsPrompts(objectivePrompts.createTask, false);
console.log(`Generating tasks for objective with prompt`)
toastr.info('Generating tasks for objective', 'Please wait...');
const taskResponse = await generateQuietPrompt(prompt)
// Clear all existing objective tasks when generating
currentObjective.children = []
const numberedListPattern = /^\d+\./
// Create tasks from generated task list
for (const task of taskResponse.split('\n').map(x => x.trim())) {
if (task.match(numberedListPattern) != null) {
currentObjective.addTask(task.replace(numberedListPattern,"").trim())
}
}
updateUiTaskList();
setCurrentTask();
console.info(`Response for Objective: '${currentObjective.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(currentObjective.children.map(v => {return v.toSaveState()}), null, 2)} `)
toastr.success(`Generated ${currentObjective.children.length} tasks`, 'Done!');
}
// Call Quiet Generate to check if a task is completed
async function checkTaskCompleted() {
// Make sure there are tasks
if (jQuery.isEmptyObject(currentTask)) {
return
}
try {
// Wait for group to finish generating
if (selected_group) {
await waitUntilCondition(() => is_group_generating === false, 1000, 10);
}
// Another extension might be doing something with the chat, so wait for it to finish
await waitUntilCondition(() => is_send_press === false, 30000, 10);
} catch {
console.debug("Failed to wait for group to finish generating")
return;
}
checkCounter = $('#objective-check-frequency').val()
toastr.info("Checking for task completion.")
const prompt = substituteParamsPrompts(objectivePrompts.checkTaskCompleted, false);
const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase()
// Check response if task complete
if (taskResponse.includes("true")) {
console.info(`Character determined task '${currentTask.description} is completed.`)
currentTask.completeTask()
} else if (!(taskResponse.includes("false"))) {
console.warn(`checkTaskCompleted response did not contain true or false. taskResponse: ${taskResponse}`)
} else {
console.debug(`Checked task completion. taskResponse: ${taskResponse}`)
}
}
function getNextIncompleteTaskRecurse(task){
if (task.completed === false // Return task if incomplete
&& task.children.length === 0 // Ensure task has no children, it's subtasks will determine completeness
&& task.parentId !== "" // Must have parent id. Only root task will be missing this and we dont want that
){
return task
}
for (const childTask of task.children) {
if (childTask.completed === true){ // Don't recurse into completed tasks
continue
}
const foundTask = getNextIncompleteTaskRecurse(childTask);
if (foundTask != null) {
return foundTask;
}
}
return null;
}
// Set a task in extensionPrompt context. Defaults to first incomplete
function setCurrentTask(taskId = null, skipSave = false) {
const context = getContext();
// TODO: Should probably null this rather than set empty object
currentTask = {};
// Find the task, either next incomplete, or by provided taskId
if (taskId === null) {
currentTask = getNextIncompleteTaskRecurse(taskTree) || {};
} else {
currentTask = getTaskById(taskId);
}
// Don't just check for a current task, check if it has data
const description = currentTask.description || null;
if (description) {
const extensionPromptText = substituteParamsPrompts(objectivePrompts.currentTask, true);
// Remove highlights
$('.objective-task').css({'border-color':'','border-width':''})
// Highlight current task
let highlightTask = currentTask
while (highlightTask.parentId !== ""){
if (highlightTask.descriptionSpan){
highlightTask.descriptionSpan.css({'border-color':'yellow','border-width':'2px'});
}
const parent = getTaskById(highlightTask.parentId)
highlightTask = parent
}
// Update the extension prompt
context.setExtensionPrompt(MODULE_NAME, extensionPromptText, 1, $('#objective-chat-depth').val());
console.info(`Current task in context.extensionPrompts.Objective is ${JSON.stringify(context.extensionPrompts.Objective)}`);
} else {
context.setExtensionPrompt(MODULE_NAME, '');
console.info(`No current task`);
}
// Save state if not skipping
if (!skipSave) {
saveState();
}
}
function getHighestTaskIdRecurse(task) {
let nextId = task.id;
for (const childTask of task.children) {
const childId = getHighestTaskIdRecurse(childTask);
if (childId > nextId) {
nextId = childId;
}
}
return nextId;
}
//###############################//
//# Task Class #//
//###############################//
class ObjectiveTask {
id
description
completed
parentId
children
// UI Elements
taskHtml
descriptionSpan
completedCheckbox
deleteTaskButton
addTaskButton
constructor ({id=undefined, description, completed=false, parentId=""}) {
this.description = description
this.parentId = parentId
this.children = []
this.completed = completed
// Generate a new ID if none specified
if (id==undefined){
this.id = getHighestTaskIdRecurse(taskTree) + 1
} else {
this.id=id
}
}
// Accepts optional index. Defaults to adding to end of list.
addTask(description, index = null) {
index = index != null ? index: index = this.children.length
this.children.splice(index, 0, new ObjectiveTask(
{description: description, parentId: this.id}
))
saveState()
}
getIndex(){
if (this.parentId !== null) {
const parent = getTaskById(this.parentId)
const index = parent.children.findIndex(task => task.id === this.id)
if (index === -1){
throw `getIndex failed: Task '${this.description}' not found in parent task '${parent.description}'`
}
return index
} else {
throw `getIndex failed: Task '${this.description}' has no parent`
}
}
// Used to set parent to complete when all child tasks are completed
checkParentComplete() {
let all_completed = true;
if (this.parentId !== ""){
const parent = getTaskById(this.parentId);
for (const child of parent.children){
if (!child.completed){
all_completed = false;
break;
}
}
if (all_completed){
parent.completed = true;
console.info(`Parent task '${parent.description}' completed after all child tasks complated.`)
} else {
parent.completed = false;
}
}
}
// Complete the current task, setting next task to next incomplete task
completeTask() {
this.completed = true
console.info(`Task successfully completed: ${JSON.stringify(this.description)}`)
this.checkParentComplete()
setCurrentTask()
updateUiTaskList()
}
// Add a single task to the UI and attach event listeners for user edits
addUiElement() {
const template = `
<div id="objective-task-label-${this.id}" class="flex1 checkbox_label">
<input id="objective-task-complete-${this.id}" type="checkbox">
<span class="text_pole objective-task" style="display: block" id="objective-task-description-${this.id}" contenteditable>${this.description}</span>
<div id="objective-task-delete-${this.id}" class="objective-task-button fa-solid fa-xmark fa-2x" title="Delete Task"></div>
<div id="objective-task-add-${this.id}" class="objective-task-button fa-solid fa-plus fa-2x" title="Add Task"></div>
<div id="objective-task-add-branch-${this.id}" class="objective-task-button fa-solid fa-code-fork fa-2x" title="Branch Task"></div>
</div><br>
`;
// Add the filled out template
$('#objective-tasks').append(template);
this.completedCheckbox = $(`#objective-task-complete-${this.id}`);
this.descriptionSpan = $(`#objective-task-description-${this.id}`);
this.addButton = $(`#objective-task-add-${this.id}`);
this.deleteButton = $(`#objective-task-delete-${this.id}`);
this.taskHtml = $(`#objective-task-label-${this.id}`);
this.branchButton = $(`#objective-task-add-branch-${this.id}`)
// Handle sub-task forking style
if (this.children.length > 0){
this.branchButton.css({'color':'#33cc33'})
} else {
this.branchButton.css({'color':''})
}
// Add event listeners and set properties
$(`#objective-task-complete-${this.id}`).prop('checked', this.completed);
$(`#objective-task-complete-${this.id}`).on('click', () => (this.onCompleteClick()));
$(`#objective-task-description-${this.id}`).on('keyup', () => (this.onDescriptionUpdate()));
$(`#objective-task-description-${this.id}`).on('focusout', () => (this.onDescriptionFocusout()));
$(`#objective-task-delete-${this.id}`).on('click', () => (this.onDeleteClick()));
$(`#objective-task-add-${this.id}`).on('click', () => (this.onAddClick()));
this.branchButton.on('click', () => (this.onBranchClick()))
}
onBranchClick() {
currentObjective = this
updateUiTaskList();
setCurrentTask();
}
onCompleteClick(){
this.completed = this.completedCheckbox.prop('checked')
this.checkParentComplete()
setCurrentTask();
}
onDescriptionUpdate(){
this.description = this.descriptionSpan.text();
}
onDescriptionFocusout(){
setCurrentTask();
}
onDeleteClick(){
const index = this.getIndex()
const parent = getTaskById(this.parentId)
parent.children.splice(index, 1)
updateUiTaskList()
setCurrentTask()
}
onAddClick(){
const index = this.getIndex()
const parent = getTaskById(this.parentId)
parent.addTask("", index + 1);
updateUiTaskList();
setCurrentTask();
}
toSaveStateRecurse() {
let children = []
if (this.children.length > 0){
for (const child of this.children){
children.push(child.toSaveStateRecurse())
}
}
return {
"id":this.id,
"description":this.description,
"completed":this.completed,
"parentId": this.parentId,
"children": children,
}
}
}
//###############################//
//# Custom Prompts #//
//###############################//
function onEditPromptClick() {
let popupText = ''
popupText += `
<div class="objective_prompt_modal">
<small>Edit prompts used by Objective for this session. You can use {{objective}} or {{task}} plus any other standard template variables. Save template to persist changes.</small>
<br>
<div>
<label for="objective-prompt-generate">Generation Prompt</label>
<textarea id="objective-prompt-generate" type="text" class="text_pole textarea_compact" rows="8"></textarea>
<label for="objective-prompt-check">Completion Check Prompt</label>
<textarea id="objective-prompt-check" type="text" class="text_pole textarea_compact" rows="8"></textarea>
<label for="objective-prompt-extension-prompt">Injected Prompt</label>
<textarea id="objective-prompt-extension-prompt" type="text" class="text_pole textarea_compact" rows="8"></textarea>
</div>
<div class="objective_prompt_block">
<label for="objective-custom-prompt-select">Custom Prompt Select</label>
<select id="objective-custom-prompt-select"><select>
</div>
<div class="objective_prompt_block">
<input id="objective-custom-prompt-new" class="menu_button" type="submit" value="New Prompt" />
<input id="objective-custom-prompt-save" class="menu_button" type="submit" value="Save Prompt" />
<input id="objective-custom-prompt-delete" class="menu_button" type="submit" value="Delete Prompt" />
</div>
</div>`
callPopup(popupText, 'text')
populateCustomPrompts()
// Set current values
$('#objective-prompt-generate').val(objectivePrompts.createTask)
$('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted)
$('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask)
// Handle value updates
$('#objective-prompt-generate').on('input', () => {
objectivePrompts.createTask = $('#objective-prompt-generate').val()
})
$('#objective-prompt-check').on('input', () => {
objectivePrompts.checkTaskCompleted = $('#objective-prompt-check').val()
})
$('#objective-prompt-extension-prompt').on('input', () => {
objectivePrompts.currentTask = $('#objective-prompt-extension-prompt').val()
})
// Handle new
$('#objective-custom-prompt-new').on('click', () => {
newCustomPrompt()
})
// Handle save
$('#objective-custom-prompt-save').on('click', () => {
saveCustomPrompt()
})
// Handle delete
$('#objective-custom-prompt-delete').on('click', () => {
deleteCustomPrompt()
})
// Handle load
$('#objective-custom-prompt-select').on('change', loadCustomPrompt)
}
async function newCustomPrompt() {
const customPromptName = await callPopup('<h3>Custom Prompt name:</h3>', 'input');
if (customPromptName == "") {
toastr.warning("Please set custom prompt name to save.")
return
}
if (customPromptName == "default"){
toastr.error("Cannot save over default prompt")
return
}
extension_settings.objective.customPrompts[customPromptName] = {}
Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts)
saveSettingsDebounced()
populateCustomPrompts()
}
function saveCustomPrompt() {
const customPromptName = $("#objective-custom-prompt-select").find(':selected').val()
if (customPromptName == "default"){
toastr.error("Cannot save over default prompt")
return
}
Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts)
saveSettingsDebounced()
populateCustomPrompts()
}
function deleteCustomPrompt(){
const customPromptName = $("#objective-custom-prompt-select").find(':selected').val()
if (customPromptName == "default"){
toastr.error("Cannot delete default prompt")
return
}
delete extension_settings.objective.customPrompts[customPromptName]
saveSettingsDebounced()
populateCustomPrompts()
loadCustomPrompt()
}
function loadCustomPrompt(){
const optionSelected = $("#objective-custom-prompt-select").find(':selected').val()
Object.assign(objectivePrompts, extension_settings.objective.customPrompts[optionSelected])
$('#objective-prompt-generate').val(objectivePrompts.createTask)
$('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted)
$('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask)
}
function populateCustomPrompts(){
// Populate saved prompts
$('#objective-custom-prompt-select').empty()
for (const customPromptName in extension_settings.objective.customPrompts){
const option = document.createElement('option');
option.innerText = customPromptName;
option.value = customPromptName;
option.selected = customPromptName
$('#objective-custom-prompt-select').append(option)
}
}
//###############################//
//# UI AND Settings #//
//###############################//
const defaultSettings = {
currentObjectiveId: null,
taskTree: null,
chatDepth: 2,
checkFrequency: 3,
hideTasks: false,
prompts: defaultPrompts,
}
// Convenient single call. Not much at the moment.
function resetState() {
lastMessageWasSwipe = false
loadSettings();
}
//
function saveState() {
const context = getContext();
if (currentChatId == "") {
currentChatId = context.chatId
}
chat_metadata['objective'] = {
currentObjectiveId: currentObjective.id,
taskTree: taskTree.toSaveStateRecurse(),
checkFrequency: $('#objective-check-frequency').val(),
chatDepth: $('#objective-chat-depth').val(),
hideTasks: $('#objective-hide-tasks').prop('checked'),
prompts: objectivePrompts,
}
saveMetadataDebounced();
}
// Dump core state
function debugObjectiveExtension() {
console.log(JSON.stringify({
"currentTask": currentTask,
"currentObjective": currentObjective,
"taskTree": taskTree.toSaveStateRecurse(),
"chat_metadata": chat_metadata['objective'],
"extension_settings": extension_settings['objective'],
"prompts": objectivePrompts
}, null, 2))
}
window.debugObjectiveExtension = debugObjectiveExtension
// Populate UI task list
function updateUiTaskList() {
$('#objective-tasks').empty()
// Show button to navigate back to parent objective if parent exists
if (currentObjective){
if (currentObjective.parentId !== "") {
$('#objective-parent').show()
} else {
$('#objective-parent').hide()
}
}
$('#objective-text').val(currentObjective.description)
if (currentObjective.children.length > 0){
// Show tasks if there are any to show
for (const task of currentObjective.children) {
task.addUiElement()
}
} else {
// Show button to add tasks if there are none
$('#objective-tasks').append(`
<input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
`)
$("#objective-task-add-first").on('click', () => {
currentObjective.addTask("")
setCurrentTask()
updateUiTaskList()
})
}
}
function onParentClick() {
currentObjective = getTaskById(currentObjective.parentId)
updateUiTaskList()
setCurrentTask()
}
// Trigger creation of new tasks with given objective.
async function onGenerateObjectiveClick() {
await generateTasks()
saveState()
}
// Update extension prompts
function onChatDepthInput() {
saveState()
setCurrentTask() // Ensure extension prompt is updated
}
function onObjectiveTextFocusOut(){
if (currentObjective){
currentObjective.description = $('#objective-text').val()
saveState()
}
}
// Update how often we check for task completion
function onCheckFrequencyInput() {
checkCounter = $("#objective-check-frequency").val()
$('#objective-counter').text(checkCounter)
saveState()
}
function onHideTasksInput() {
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
saveState()
}
function loadTaskChildrenRecurse(savedTask) {
let tempTaskTree = new ObjectiveTask({
id: savedTask.id,
description: savedTask.description,
completed: savedTask.completed,
parentId: savedTask.parentId,
})
for (const task of savedTask.children){
const childTask = loadTaskChildrenRecurse(task)
tempTaskTree.children.push(childTask)
}
return tempTaskTree
}
function loadSettings() {
// Load/Init settings for chatId
currentChatId = getContext().chatId
// Reset Objectives and Tasks in memory
taskTree = null;
currentObjective = null;
// Init extension settings
if (Object.keys(extension_settings.objective).length === 0) {
Object.assign(extension_settings.objective, { 'customPrompts': {'default':defaultPrompts}})
}
// Bail on home screen
if (currentChatId == undefined) {
return
}
// Migrate existing settings
if (currentChatId in extension_settings.objective) {
// TODO: Remove this soon
chat_metadata['objective'] = extension_settings.objective[currentChatId];
delete extension_settings.objective[currentChatId];
}
if (!('objective' in chat_metadata)) {
Object.assign(chat_metadata, { objective: defaultSettings });
}
// Migrate legacy flat objective to new objectiveTree and currentObjective
if ('objective' in chat_metadata.objective) {
// Create root objective from legacy objective
taskTree = new ObjectiveTask({id:0, description: chat_metadata.objective.objective});
currentObjective = taskTree;
// Populate root objective tree from legacy tasks
if ('tasks' in chat_metadata.objective) {
let idIncrement = 0;
taskTree.children = chat_metadata.objective.tasks.map(task => {
idIncrement += 1;
return new ObjectiveTask({
id: idIncrement,
description: task.description,
completed: task.completed,
parentId: taskTree.id,
})
});
}
saveState();
delete chat_metadata.objective.objective;
delete chat_metadata.objective.tasks;
} else {
// Load Objectives and Tasks (Normal path)
if (chat_metadata.objective.taskTree){
taskTree = loadTaskChildrenRecurse(chat_metadata.objective.taskTree)
}
}
// Make sure there's a root task
if (!taskTree) {
taskTree = new ObjectiveTask({id:0,description:$('#objective-text').val()})
}
currentObjective = taskTree
checkCounter = chat_metadata['objective'].checkFrequency
// Update UI elements
$('#objective-counter').text(checkCounter)
$("#objective-text").text(taskTree.description)
updateUiTaskList()
$('#objective-chat-depth').val(chat_metadata['objective'].chatDepth)
$('#objective-check-frequency').val(chat_metadata['objective'].checkFrequency)
$('#objective-hide-tasks').prop('checked', chat_metadata['objective'].hideTasks)
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
setCurrentTask(null, true)
}
function addManualTaskCheckUi() {
$('#extensionsMenu').prepend(`
<div id="objective-task-manual-check-menu-item" class="list-group-item flex-container flexGap5">
<div id="objective-task-manual-check" class="extensionsMenuExtensionButton fa-regular fa-square-check"/></div>
Manual Task Check
</div>`)
$('#objective-task-manual-check-menu-item').attr('title', 'Trigger AI check of completed tasks').on('click', checkTaskCompleted)
}
jQuery(() => {
const settingsHtml = `
<div class="objective-settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Objective</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
<textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
<div class="objective_block flex-container">
<input id="objective-generate" class="menu_button" type="submit" value="Auto-Generate Tasks" />
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label>
</div>
<div id="objective-parent" class="objective_block flex-container">
<i class="objective-task-button fa-solid fa-circle-left fa-2x" title="Go to Parent"></i>
<small>Go to parent task</small>
</div>
<div id="objective-tasks"> </div>
<div class="objective_block margin-bot-10px">
<div class="objective_block objective_block_control flex1 flexFlowColumn">
<label for="objective-chat-depth">Position in Chat</label>
<input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</div>
<br>
<div class="objective_block objective_block_control flex1">
<label for="objective-check-frequency">Task Check Frequency</label>
<input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="0" max="99" />
<small>(0 = disabled)</small>
</div>
</div>
<span> Messages until next AI task completion check <span id="objective-counter">0</span></span>
<div class="objective_block flex-container">
<input id="objective_prompt_edit" class="menu_button" type="submit" value="Edit Prompts" />
</div>
<hr class="sysHR">
</div>
</div>
</div>
`;
addManualTaskCheckUi()
$('#extensions_settings').append(settingsHtml);
$('#objective-generate').on('click', onGenerateObjectiveClick)
$('#objective-chat-depth').on('input', onChatDepthInput)
$("#objective-check-frequency").on('input', onCheckFrequencyInput)
$('#objective-hide-tasks').on('click', onHideTasksInput)
$('#objective_prompt_edit').on('click', onEditPromptClick)
$('#objective-parent').hide()
$('#objective-parent').on('click',onParentClick)
$('#objective-text').on('focusout',onObjectiveTextFocusOut)
loadSettings()
eventSource.on(event_types.CHAT_CHANGED, () => {
resetState()
});
eventSource.on(event_types.MESSAGE_SWIPED, () => {
lastMessageWasSwipe = true
})
eventSource.on(event_types.MESSAGE_RECEIVED, () => {
if (currentChatId == undefined || jQuery.isEmptyObject(currentTask) || lastMessageWasSwipe) {
lastMessageWasSwipe = false
return
}
if ($("#objective-check-frequency").val() > 0) {
// Check only at specified interval
if (checkCounter <= 0) {
checkTaskCompleted();
}
checkCounter -= 1
}
setCurrentTask();
$('#objective-counter').text(checkCounter)
});
registerSlashCommand('taskcheck', checkTaskCompleted, [], ' checks if the current task is completed', true, true);
});

View File

@ -1,11 +0,0 @@
{
"display_name": "Objective",
"loading_order": 5,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Ouoertheo",
"version": "0.0.1",
"homePage": ""
}

View File

@ -1,52 +0,0 @@
#objective-counter {
font-weight: 600;
color: orange;
}
.objective_block {
display: flex;
align-items: center;
column-gap: 5px;
flex-wrap: wrap;
}
.objective_prompt_block {
display: flex;
align-items: baseline;
column-gap: 5px;
flex-wrap: wrap;
}
.objective_block_control {
align-items: baseline;
}
.objective_block_control small,
.objective_block_control label {
width: max-content;
}
.objective-task-button {
margin: 0;
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
align-items: center;
justify-content: center;
}
.objective-task-button:hover {
opacity: 1;
}
[id^=objective-task-delete-] {
color: #da3f3f;
}
#objective-tasks span {
margin: unset;
margin-bottom: 5px !important;
}

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,9 +155,9 @@ 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 '/'
if (!extension_settings.quickReply.quickActionEnabled || prompt.startsWith('/')) { if (!extension_settings.quickReply.quickActionEnabled || prompt.startsWith('/')) {
$("#send_but").trigger('click'); $("#send_but").trigger('click');
@ -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>
@ -379,10 +399,11 @@ jQuery(async () => {
</div>`; </div>`;
$('#extensions_settings2').append(settingsHtml); $('#extensions_settings2').append(settingsHtml);
// 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

@ -1,152 +0,0 @@
import { saveSettingsDebounced } from "../../../script.js";
import { extension_settings } from "../../extensions.js";
function toggleRandomizedSetting(buttonRef, forId) {
if (extension_settings.randomizer.controls.indexOf(forId) === -1) {
extension_settings.randomizer.controls.push(forId);
} else {
extension_settings.randomizer.controls = extension_settings.randomizer.controls.filter(x => x !== forId);
}
buttonRef.toggleClass('active');
console.debug('Randomizer controls:', extension_settings.randomizer.controls);
saveSettingsDebounced();
}
function addRandomizeButton() {
const counterRef = $(this);
const labelRef = $(this).find('div[data-for]');
const isDisabled = counterRef.data('randomization-disabled');
if (labelRef.length === 0 || isDisabled == true) {
return;
}
const forId = labelRef.data('for');
const buttonRef = $('<div class="randomize_button menu_button fa-solid fa-shuffle"></div>');
buttonRef.toggleClass('active', extension_settings.randomizer.controls.indexOf(forId) !== -1);
buttonRef.hide();
buttonRef.on('click', () => toggleRandomizedSetting(buttonRef, forId));
counterRef.append(buttonRef);
}
function onRandomizerEnabled() {
extension_settings.randomizer.enabled = $(this).prop('checked');
$('.randomize_button').toggle(extension_settings.randomizer.enabled);
console.debug('Randomizer enabled:', extension_settings.randomizer.enabled);
}
window['randomizerInterceptor'] = (function () {
if (extension_settings.randomizer.enabled === false) {
console.debug('Randomizer skipped: disabled.');
return;
}
if (extension_settings.randomizer.fluctuation === 0 || extension_settings.randomizer.controls.length === 0) {
console.debug('Randomizer skipped: nothing to do.');
return;
}
for (const control of extension_settings.randomizer.controls) {
const controlRef = $('#' + control);
if (controlRef.length === 0) {
console.debug(`Randomizer skipped: control ${control} not found.`);
continue;
}
if (!controlRef.is(':visible')) {
console.debug(`Randomizer skipped: control ${control} is not visible.`);
continue;
}
let previousValue = parseFloat(controlRef.data('previous-value'));
let originalValue = parseFloat(controlRef.data('original-value'));
let currentValue = parseFloat(controlRef.val());
let value;
// Initialize originalValue and previousValue if they are NaN
if (isNaN(originalValue)) {
originalValue = currentValue;
controlRef.data('original-value', originalValue);
}
if (isNaN(previousValue)) {
previousValue = currentValue;
controlRef.data('previous-value', previousValue);
}
// If the current value hasn't changed compared to the previous value, use the original value as a base for the calculation
if (currentValue === previousValue) {
console.debug(`Randomizer for ${control} reusing original value: ${originalValue}`);
value = originalValue;
} else {
console.debug(`Randomizer for ${control} using current value: ${currentValue}`);
value = currentValue;
controlRef.data('previous-value', currentValue); // Update the previous value when using the current value
controlRef.data('original-value', currentValue); // Update the original value when using the current value
}
if (isNaN(value)) {
console.debug('Randomizer skipped: NaN.');
continue;
}
const fluctuation = extension_settings.randomizer.fluctuation;
const min = parseFloat(controlRef.attr('min'));
const max = parseFloat(controlRef.attr('max'));
const delta = (Math.random() * fluctuation * 2 - fluctuation) * value;
const newValue = Math.min(Math.max(value + delta, min), max);
console.debug(`Randomizer for ${control}: ${value} -> ${newValue} (delta: ${delta}, min: ${min}, max: ${max})`);
controlRef.val(newValue).trigger('input');
controlRef.data('previous-value', parseFloat(controlRef.val()));
}
});
jQuery(() => {
const html = `
<div class="randomizer_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Parameter Randomizer</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="randomizer_enabled" class="checkbox_label">
<input type="checkbox" id="randomizer_enabled" name="randomizer_enabled" >
Enabled
</label>
<div class="range-block">
<div class="range-block-title">
Fluctuation (0-1)
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="randomizer_fluctuation" min="0" max="1" step="0.1">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="randomizer_fluctuation" id="randomizer_fluctuation_counter">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>`;
$('#extensions_settings2').append(html);
$('#ai_response_configuration .range-block-counter').each(addRandomizeButton);
$('#randomizer_enabled').on('input', onRandomizerEnabled);
$('#randomizer_enabled').prop('checked', extension_settings.randomizer.enabled).trigger('input');
$('#randomizer_fluctuation').val(extension_settings.randomizer.fluctuation).trigger('input');
$('#randomizer_fluctuation_counter').text(extension_settings.randomizer.fluctuation);
$('#randomizer_fluctuation').on('input', function () {
const value = parseFloat($(this).val());
$('#randomizer_fluctuation_counter').text(value);
extension_settings.randomizer.fluctuation = value;
console.debug('Randomizer fluctuation:', extension_settings.randomizer.fluctuation);
saveSettingsDebounced();
});
});

View File

@ -1,12 +0,0 @@
{
"display_name": "Parameter Randomizer",
"loading_order": 15,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"generate_interceptor": "randomizerInterceptor",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,489 +0,0 @@
/*
TODO:
- load RVC models list from extras
- Settings per characters
*/
import { saveSettingsDebounced } from "../../../script.js";
import { getContext, getApiUrl, extension_settings, doExtrasFetch, ModuleWorkerWrapper, modules } from "../../extensions.js";
export { MODULE_NAME, rvcVoiceConversion };
const MODULE_NAME = 'RVC';
const DEBUG_PREFIX = "<RVC module> "
const UPDATE_INTERVAL = 1000
let charactersList = [] // Updated with module worker
let rvcModelsList = [] // Initialized only once
let rvcModelsReceived = false;
function updateVoiceMapText() {
let voiceMapText = ""
for (let i in extension_settings.rvc.voiceMap) {
const voice_settings = extension_settings.rvc.voiceMap[i];
voiceMapText += i + ":"
+ voice_settings["modelName"] + "("
+ voice_settings["pitchExtraction"] + ","
+ voice_settings["pitchOffset"] + ","
+ voice_settings["indexRate"] + ","
+ voice_settings["filterRadius"] + ","
+ voice_settings["rmsMixRate"] + ","
+ voice_settings["protect"]
+ "),\n"
}
extension_settings.rvc.voiceMapText = voiceMapText;
$('#rvc_voice_map').val(voiceMapText);
console.debug(DEBUG_PREFIX, "Updated voice map debug text to\n", voiceMapText)
}
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
enabled: false,
model: "",
pitchOffset: 0,
pitchExtraction: "dio",
indexRate: 0.88,
filterRadius: 3,
rmsMixRate: 1,
protect: 0.33,
voicMapText: "",
voiceMap: {}
}
function loadSettings() {
if (extension_settings.rvc === undefined)
extension_settings.rvc = {};
if (Object.keys(extension_settings.rvc).length === 0) {
Object.assign(extension_settings.rvc, defaultSettings)
}
$('#rvc_enabled').prop('checked', extension_settings.rvc.enabled);
$('#rvc_model').val(extension_settings.rvc.model);
$('#rvc_pitch_extraction').val(extension_settings.rvc.pitchExtraction);
$('#rvc_pitch_extractiont_value').text(extension_settings.rvc.pitchExtraction);
$('#rvc_index_rate').val(extension_settings.rvc.indexRate);
$('#rvc_index_rate_value').text(extension_settings.rvc.indexRate);
$('#rvc_filter_radius').val(extension_settings.rvc.filterRadius);
$("#rvc_filter_radius_value").text(extension_settings.rvc.filterRadius);
$('#rvc_pitch_offset').val(extension_settings.rvc.pitchOffset);
$('#rvc_pitch_offset_value').text(extension_settings.rvc.pitchOffset);
$('#rvc_rms_mix_rate').val(extension_settings.rvc.rmsMixRate);
$("#rvc_rms_mix_rate_value").text(extension_settings.rvc.rmsMixRate);
$('#rvc_protect').val(extension_settings.rvc.protect);
$("#rvc_protect_value").text(extension_settings.rvc.protect);
$('#rvc_voice_map').val(extension_settings.rvc.voiceMapText);
}
async function onEnabledClick() {
extension_settings.rvc.enabled = $('#rvc_enabled').is(':checked');
saveSettingsDebounced()
}
async function onPitchExtractionChange() {
extension_settings.rvc.pitchExtraction = $('#rvc_pitch_extraction').val();
saveSettingsDebounced()
}
async function onIndexRateChange() {
extension_settings.rvc.indexRate = Number($('#rvc_index_rate').val());
$("#rvc_index_rate_value").text(extension_settings.rvc.indexRate)
saveSettingsDebounced()
}
async function onFilterRadiusChange() {
extension_settings.rvc.filterRadius = Number($('#rvc_filter_radius').val());
$("#rvc_filter_radius_value").text(extension_settings.rvc.filterRadius)
saveSettingsDebounced()
}
async function onPitchOffsetChange() {
extension_settings.rvc.pitchOffset = Number($('#rvc_pitch_offset').val());
$("#rvc_pitch_offset_value").text(extension_settings.rvc.pitchOffset)
saveSettingsDebounced()
}
async function onRmsMixRateChange() {
extension_settings.rvc.rmsMixRate = Number($('#rvc_rms_mix_rate').val());
$("#rvc_rms_mix_rate_value").text(extension_settings.rvc.rmsMixRate)
saveSettingsDebounced()
}
async function onProtectChange() {
extension_settings.rvc.protect = Number($('#rvc_protect').val());
$("#rvc_protect_value").text(extension_settings.rvc.protect)
saveSettingsDebounced()
}
async function onApplyClick() {
let error = false;
const character = $("#rvc_character_select").val();
const model_name = $("#rvc_model_select").val();
const pitchExtraction = $("#rvc_pitch_extraction").val();
const indexRate = $("#rvc_index_rate").val();
const filterRadius = $("#rvc_filter_radius").val();
const pitchOffset = $("#rvc_pitch_offset").val();
const rmsMixRate = $("#rvc_rms_mix_rate").val();
const protect = $("#rvc_protect").val();
if (character === "none") {
toastr.error("Character not selected.", DEBUG_PREFIX + " voice mapping apply", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
if (model_name == "none") {
toastr.error("Model not selected.", DEBUG_PREFIX + " voice mapping apply", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
extension_settings.rvc.voiceMap[character] = {
"modelName": model_name,
"pitchExtraction": pitchExtraction,
"indexRate": indexRate,
"filterRadius": filterRadius,
"pitchOffset": pitchOffset,
"rmsMixRate": rmsMixRate,
"protect": protect
}
updateVoiceMapText();
console.debug(DEBUG_PREFIX, "Updated settings of ", character, ":", extension_settings.rvc.voiceMap[character])
saveSettingsDebounced();
}
async function onDeleteClick() {
const character = $("#rvc_character_select").val();
if (character === "none") {
toastr.error("Character not selected.", DEBUG_PREFIX + " voice mapping delete", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
delete extension_settings.rvc.voiceMap[character];
console.debug(DEBUG_PREFIX, "Deleted settings of ", character);
updateVoiceMapText();
saveSettingsDebounced();
}
async function onChangeUploadFiles() {
const url = new URL(getApiUrl());
const inputFiles = $("#rvc_model_upload_files").get(0).files;
let formData = new FormData();
for (const file of inputFiles)
formData.append(file.name, file);
console.debug(DEBUG_PREFIX, "Sending files:", formData);
url.pathname = '/api/voice-conversion/rvc/upload-models';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
body: formData
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Check extras console for errors log');
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
alert('The files have been uploaded successfully.');
}
$(document).ready(function () {
function addExtensionControls() {
const settingsHtml = `
<div id="rvc_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>RVC</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<h4 class="center">Characters Voice Mapping</h4>
<div>
<label class="checkbox_label" for="rvc_enabled">
<input type="checkbox" id="rvc_enabled" name="rvc_enabled">
<small>Enabled</small>
</label>
<label>Voice Map (debug infos)</label>
<textarea id="rvc_voice_map" type="text" class="text_pole textarea_compact" rows="4"
placeholder="Voice map will appear here for debug purpose"></textarea>
</div>
<div>
<div class="background_controls">
<label for="rvc_character_select">Character:</label>
<select id="rvc_character_select">
<!-- Populated by JS -->
</select>
<div id="rvc_delete" class="menu_button">
<i class="fa-solid fa-times"></i>
Remove
</div>
</div>
<div class="background_controls">
<label for="rvc_model_select">Voice:</label>
<select id="rvc_model_select">
<!-- Populated by JS -->
</select>
<div id="rvc_model_refresh_button" class="menu_button">
<i class="fa-solid fa-refresh"></i>
<!-- Refresh -->
</div>
<div id="rvc_model_upload_select_button" class="menu_button">
<i class="fa-solid fa-upload"></i>
Upload
</div>
<input
type="file"
id="rvc_model_upload_files"
accept=".zip,.rar,.7zip,.7z" multiple />
</div>
</div>
<div>
<small>
Upload one archive per model. With .pth and .index (optional) inside.<br/>
Supported format: .zip .rar .7zip .7z
</small>
</div>
<div>
<h4>Model Settings</h4>
</div>
<div>
<label for="rvc_pitch_extraction">
Pitch Extraction
</label>
<select id="rvc_pitch_extraction">
<option value="dio">dio</option>
<option value="pm">pm</option>
<option value="harvest">harvest</option>
<option value="torchcrepe">torchcrepe</option>
<option value="rmvpe">rmvpe</option>
<option value="">None</option>
</select>
<small>
Tips: dio and pm faster, harvest slower but good.<br/>
Torchcrepe and rmvpe are good but uses GPU.
</small>
</div>
<div>
<label for="rvc_index_rate">
Search feature ratio (<span id="rvc_index_rate_value"></span>)
</label>
<input id="rvc_index_rate" type="range" min="0" max="1" step="0.01" value="0.5" />
<small>
Controls accent strength, too high may produce artifact.
</small>
</div>
<div>
<label for="rvc_filter_radius">Filter radius (<span id="rvc_filter_radius_value"></span>)</label>
<input id="rvc_filter_radius" type="range" min="0" max="7" step="1" value="3" />
<small>
Higher can reduce breathiness but may increase run time.
</small>
</div>
<div>
<label for="rvc_pitch_offset">Pitch offset (<span id="rvc_pitch_offset_value"></span>)</label>
<input id="rvc_pitch_offset" type="range" min="-20" max="20" step="1" value="0" />
<small>
Recommended +12 key for male to female conversion and -12 key for female to male conversion.
</small>
</div>
<div>
<label for="rvc_rms_mix_rate">Mix rate (<span id="rvc_rms_mix_rate_value"></span>)</label>
<input id="rvc_rms_mix_rate" type="range" min="0" max="1" step="0.01" value="1" />
<small>
Closer to 0 is closer to TTS and 1 is closer to trained voice.
Can help mask noise and sound more natural when set relatively low.
</small>
</div>
<div>
<label for="rvc_protect">Protect amount (<span id="rvc_protect_value"></span>)</label>
<input id="rvc_protect" type="range" min="0" max="1" step="0.01" value="0.33" />
<small>
Avoid non voice sounds. Lower is more being ignored.
</small>
</div>
<div id="rvc_status">
</div>
<div class="rvc_buttons">
<input id="rvc_apply" class="menu_button" type="submit" value="Apply" />
</div>
</div>
</div>
</div>
</div>
`;
$('#extensions_settings').append(settingsHtml);
$("#rvc_enabled").on("click", onEnabledClick);
$("#rvc_voice_map").attr("disabled", "disabled");;
$('#rvc_pitch_extraction').on('change', onPitchExtractionChange);
$('#rvc_index_rate').on('input', onIndexRateChange);
$('#rvc_filter_radius').on('input', onFilterRadiusChange);
$('#rvc_pitch_offset').on('input', onPitchOffsetChange);
$('#rvc_rms_mix_rate').on('input', onRmsMixRateChange);
$('#rvc_protect').on('input', onProtectChange);
$("#rvc_apply").on("click", onApplyClick);
$("#rvc_delete").on("click", onDeleteClick);
$("#rvc_model_upload_files").hide();
$("#rvc_model_upload_select_button").on("click", function() {$("#rvc_model_upload_files").click()});
$("#rvc_model_upload_files").on("change", onChangeUploadFiles);
//$("#rvc_model_upload_button").on("click", onClickUpload);
$("#rvc_model_refresh_button").on("click", refreshVoiceList);
}
addExtensionControls(); // No init dependencies
loadSettings(); // Depends on Extension Controls
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
moduleWorker();
})
//#############################//
// API Calls //
//#############################//
/*
Check model installation state, return one of ["installed", "corrupted", "absent"]
*/
async function get_models_list(model_id) {
const url = new URL(getApiUrl());
url.pathname = '/api/voice-conversion/rvc/get-models-list';
const apiResult = await doExtrasFetch(url, {
method: 'POST'
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Check model state request failed');
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
return apiResult
}
/*
Send an audio file to RVC to convert voice
*/
async function rvcVoiceConversion(response, character, text) {
let apiResult
// Check voice map
if (extension_settings.rvc.voiceMap[character] === undefined) {
//toastr.error("No model is assigned to character '"+character+"', check RVC voice map in the extension menu.", DEBUG_PREFIX+'RVC Voice map error', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
console.info(DEBUG_PREFIX, "No RVC model assign in voice map for current character " + character);
return response;
}
const audioData = await response.blob()
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']) {
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
}
console.log("Audio type received:", audioData.type)
const voice_settings = extension_settings.rvc.voiceMap[character];
var requestData = new FormData();
requestData.append('AudioFile', audioData, 'record');
requestData.append("json", JSON.stringify({
"modelName": voice_settings["modelName"],
"pitchExtraction": voice_settings["pitchExtraction"],
"pitchOffset": voice_settings["pitchOffset"],
"indexRate": voice_settings["indexRate"],
"filterRadius": voice_settings["filterRadius"],
"rmsMixRate": voice_settings["rmsMixRate"],
"protect": voice_settings["protect"],
"text": text
}));
console.log("Sending tts audio data to RVC on extras server",requestData)
const url = new URL(getApiUrl());
url.pathname = '/api/voice-conversion/rvc/process-audio';
apiResult = await doExtrasFetch(url, {
method: 'POST',
body: requestData,
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' RVC Voice Conversion Failed', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
return apiResult;
}
//#############################//
// Module Worker //
//#############################//
async function refreshVoiceList() {
let result = await get_models_list();
result = await result.json();
rvcModelsList = result["models_list"]
$('#rvc_model_select')
.find('option')
.remove()
.end()
.append('<option value="none">Select Voice</option>')
.val('none')
for (const modelName of rvcModelsList) {
$("#rvc_model_select").append(new Option(modelName, modelName));
}
rvcModelsReceived = true
console.debug(DEBUG_PREFIX, "Updated model list to:", rvcModelsList);
}
async function moduleWorker() {
updateCharactersList();
if (modules.includes('rvc') && !rvcModelsReceived) {
refreshVoiceList();
}
}
function updateCharactersList() {
let currentcharacters = new Set();
const context = getContext();
for (const i of context.characters) {
currentcharacters.add(i.name);
}
currentcharacters = Array.from(currentcharacters);
currentcharacters.unshift(context.name1);
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
charactersList = currentcharacters
$('#rvc_character_select')
.find('option')
.remove()
.end()
.append('<option value="none">Select Character</option>')
.val('none')
for (const charName of charactersList) {
$("#rvc_character_select").append(new Option(charName, charName));
}
console.debug(DEBUG_PREFIX, "Updated character list to:", charactersList);
}
}

View File

@ -1,11 +0,0 @@
{
"display_name": "RVC",
"loading_order": 13,
"requires": ["rvc"],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Keij#6799",
"version": "0.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,3 +0,0 @@
.speech-toggle {
display: flex;
}

View File

@ -1,11 +0,0 @@
{
"display_name": "Settings Search",
"loading_order": 15,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "RossAscends",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,5 +0,0 @@
.highlighted {
color: black;
background-color: yellow;
text-shadow: none !important;
}

View File

@ -1,233 +0,0 @@
// Borrowed from Agnai (AGPLv3)
// https://github.com/agnaistic/agnai/blob/dev/web/pages/Chat/components/SpeechRecognitionRecorder.tsx
// First version by Cohee#1207
// Adapted by Tony-sama
export { BrowserSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Browser)> "
class BrowserSttProvider {
//########//
// Config //
//########//
settings = {
language: ""
}
defaultSettings = {
language: "en-US",
}
processTranscriptFunction = null;
get settingsHtml() {
let html = ' \
<span>Language</span> </br> \
<select id="speech_recognition_browser_provider_language"> \
<option value="ar-SA">ar-SA: Arabic (Saudi Arabia)</option> \
<option value="bn-BD">bn-BD: Bangla (Bangladesh)</option> \
<option value="bn-IN">bn-IN: Bangla (India)</option> \
<option value="cs-CZ">cs-CZ: Czech (Czech Republic)</option> \
<option value="da-DK">da-DK: Danish (Denmark)</option> \
<option value="de-AT">de-AT: German (Austria)</option> \
<option value="de-CH">de-CH: German (Switzerland)</option> \
<option value="de-DE">de-DE: German (Germany)</option> \
<option value="el-GR">el-GR: Greek (Greece)</option> \
<option value="en-AU">en-AU: English (Australia)</option> \
<option value="en-CA">en-CA: English (Canada)</option> \
<option value="en-GB">en-GB: English (United Kingdom)</option> \
<option value="en-IE">en-IE: English (Ireland)</option> \
<option value="en-IN">en-IN: English (India)</option> \
<option value="en-NZ">en-NZ: English (New Zealand)</option> \
<option value="en-US">en-US: English (United States)</option> \
<option value="en-ZA">en-ZA: English (South Africa)</option> \
<option value="es-AR">es-AR: Spanish (Argentina)</option> \
<option value="es-CL">es-CL: Spanish (Chile)</option> \
<option value="es-CO">es-CO: Spanish (Columbia)</option> \
<option value="es-ES">es-ES: Spanish (Spain)</option> \
<option value="es-MX">es-MX: Spanish (Mexico)</option> \
<option value="es-US">es-US: Spanish (United States)</option> \
<option value="fi-FI">fi-FI: Finnish (Finland)</option> \
<option value="fr-BE">fr-BE: French (Belgium)</option> \
<option value="fr-CA">fr-CA: French (Canada)</option> \
<option value="fr-CH">fr-CH: French (Switzerland)</option> \
<option value="fr-FR">fr-FR: French (France)</option> \
<option value="he-IL">he-IL: Hebrew (Israel)</option> \
<option value="hi-IN">hi-IN: Hindi (India)</option> \
<option value="hu-HU">hu-HU: Hungarian (Hungary)</option> \
<option value="id-ID">id-ID: Indonesian (Indonesia)</option> \
<option value="it-CH">it-CH: Italian (Switzerland)</option> \
<option value="it-IT">it-IT: Italian (Italy)</option> \
<option value="ja-JP">ja-JP: Japanese (Japan)</option> \
<option value="ko-KR">ko-KR: Korean (Republic of Korea)</option> \
<option value="nl-BE">nl-BE: Dutch (Belgium)</option> \
<option value="nl-NL">nl-NL: Dutch (The Netherlands)</option> \
<option value="no-NO">no-NO: Norwegian (Norway)</option> \
<option value="pl-PL">pl-PL: Polish (Poland)</option> \
<option value="pt-BR">pt-BR: Portugese (Brazil)</option> \
<option value="pt-PT">pt-PT: Portugese (Portugal)</option> \
<option value="ro-RO">ro-RO: Romanian (Romania)</option> \
<option value="ru-RU">ru-RU: Russian (Russian Federation)</option> \
<option value="sk-SK">sk-SK: Slovak (Slovakia)</option> \
<option value="sv-SE">sv-SE: Swedish (Sweden)</option> \
<option value="ta-IN">ta-IN: Tamil (India)</option> \
<option value="ta-LK">ta-LK: Tamil (Sri Lanka)</option> \
<option value="th-TH">th-TH: Thai (Thailand)</option> \
<option value="tr-TR">tr-TR: Turkish (Turkey)</option> \
<option value="zh-CN">zh-CN: Chinese (China)</option> \
<option value="zh-HK">zh-HK: Chinese (Hond Kong)</option> \
<option value="zh-TW">zh-TW: Chinese (Taiwan)</option> \
</select> \
'
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.language = $("#speech_recognition_browser_provider_language").val();
console.debug(DEBUG_PREFIX+"Change language to",this.settings.language);
this.loadSettings(this.settings);
}
static capitalizeInterim(interimTranscript) {
let capitalizeIndex = -1;
if (interimTranscript.length > 2 && interimTranscript[0] === ' ') capitalizeIndex = 1;
else if (interimTranscript.length > 1) capitalizeIndex = 0;
if (capitalizeIndex > -1) {
const spacing = capitalizeIndex > 0 ? ' '.repeat(capitalizeIndex - 1) : '';
const capitalized = interimTranscript[capitalizeIndex].toLocaleUpperCase();
const rest = interimTranscript.substring(capitalizeIndex + 1);
interimTranscript = spacing + capitalized + rest;
}
return interimTranscript;
}
static composeValues(previous, interim) {
let spacing = '';
if (previous.endsWith('.')) spacing = ' ';
return previous + spacing + interim;
}
loadSettings(settings) {
const processTranscript = this.processTranscriptFunction;
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default browser STT settings")
}
// Initialise as defaultSettings
this.settings = this.defaultSettings;
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to Speech recogniton extension (browser): ${key}`
}
}
$("#speech_recognition_browser_provider_language").val(this.settings.language);
const speechRecognitionSettings = $.extend({
grammar: '' // Custom grammar
}, options);
const speechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const speechRecognitionList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
if (!speechRecognition) {
console.warn(DEBUG_PREFIX+'Speech recognition is not supported in this browser.');
$("#microphone_button").hide();
toastr.error("Speech recognition is not supported in this browser, use another browser or another provider of SillyTavern-extras Speech recognition extension.", "Speech recognition activation Failed (Browser)", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
const recognition = new speechRecognition();
if (speechRecognitionSettings.grammar && speechRecognitionList) {
speechRecognitionList.addFromString(speechRecognitionSettings.grammar, 1);
recognition.grammars = speechRecognitionList;
}
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = this.settings.language;
const textarea = $('#send_textarea');
const button = $('#microphone_button');
let listening = false;
button.off('click').on("click", function () {
if (listening) {
recognition.stop();
} else {
recognition.start();
}
listening = !listening;
});
let initialText = '';
recognition.onresult = function (speechEvent) {
let finalTranscript = '';
let interimTranscript = ''
for (let i = speechEvent.resultIndex; i < speechEvent.results.length; ++i) {
const transcript = speechEvent.results[i][0].transcript;
if (speechEvent.results[i].isFinal) {
let interim = BrowserSttProvider.capitalizeInterim(transcript);
if (interim != '') {
let final = finalTranscript;
final = BrowserSttProvider.composeValues(final, interim);
if (final.slice(-1) != '.' & final.slice(-1) != '?') final += '.';
finalTranscript = final;
recognition.abort();
listening = false;
}
interimTranscript = ' ';
} else {
interimTranscript += transcript;
}
}
interimTranscript = BrowserSttProvider.capitalizeInterim(interimTranscript);
textarea.val(initialText + finalTranscript + interimTranscript);
};
recognition.onerror = function (event) {
console.error('Error occurred in recognition:', event.error);
//if ($('#speech_recognition_debug').is(':checked'))
// toastr.error('Error occurred in recognition:'+ event.error, 'STT Generation error (Browser)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
};
recognition.onend = function () {
listening = false;
button.toggleClass('fa-microphone fa-microphone-slash');
const newText = textarea.val().substring(initialText.length);
textarea.val(textarea.val().substring(0,initialText.length));
processTranscript(newText);
};
recognition.onstart = function () {
initialText = textarea.val();
button.toggleClass('fa-microphone fa-microphone-slash');
if ($("#speech_recognition_message_mode").val() == "replace") {
textarea.val("");
initialText = ""
}
};
$("#microphone_button").show();
console.debug(DEBUG_PREFIX+"Browser STT settings loaded")
}
}

View File

@ -1,452 +0,0 @@
/*
TODO:
- try pseudo streaming audio by just sending chunk every X seconds and asking VOSK if it is full text.
*/
import { saveSettingsDebounced } from "../../../script.js";
import { getContext, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
import { VoskSttProvider } from './vosk.js'
import { WhisperSttProvider } from './whisper.js'
import { BrowserSttProvider } from './browser.js'
import { StreamingSttProvider } from './streaming.js'
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
export { MODULE_NAME };
const MODULE_NAME = 'Speech Recognition';
const DEBUG_PREFIX = "<Speech Recognition module> "
const UPDATE_INTERVAL = 100;
let inApiCall = false;
let sttProviders = {
None: null,
Browser: BrowserSttProvider,
Whisper: WhisperSttProvider,
Vosk: VoskSttProvider,
Streaming: StreamingSttProvider,
}
let sttProvider = null
let sttProviderName = "None"
let audioRecording = false
const constraints = { audio: { sampleSize: 16, channelCount: 1, sampleRate: 16000 } };
let audioChunks = [];
async function moduleWorker() {
if (sttProviderName != "Streaming") {
return;
}
// API is busy
if (inApiCall) {
return;
}
try {
inApiCall = true;
const userMessageOriginal = await sttProvider.getUserMessage();
let userMessageFormatted = userMessageOriginal.trim();
if (userMessageFormatted.length > 0)
{
console.debug(DEBUG_PREFIX+"recorded transcript: \""+userMessageFormatted+"\"");
let userMessageLower = userMessageFormatted.toLowerCase();
// remove punctuation
let userMessageRaw = userMessageLower.replace(/[^\w\s\']|_/g, "").replace(/\s+/g, " ");
console.debug(DEBUG_PREFIX+"raw transcript:",userMessageRaw);
// Detect trigger words
let messageStart = -1;
if (extension_settings.speech_recognition.Streaming.triggerWordsEnabled) {
for (const triggerWord of extension_settings.speech_recognition.Streaming.triggerWords) {
const triggerPos = userMessageRaw.indexOf(triggerWord.toLowerCase());
// Trigger word not found or not starting message and just a substring
if (triggerPos == -1){ // | (triggerPos > 0 & userMessageFormatted[triggerPos-1] != " ")) {
console.debug(DEBUG_PREFIX+"trigger word not found: ", triggerWord);
}
else {
console.debug(DEBUG_PREFIX+"Found trigger word: ", triggerWord, " at index ", triggerPos);
if (triggerPos < messageStart || messageStart == -1) { // & (triggerPos + triggerWord.length) < userMessageFormatted.length)) {
messageStart = triggerPos; // + triggerWord.length + 1;
if (!extension_settings.speech_recognition.Streaming.triggerWordsIncluded)
messageStart = triggerPos + triggerWord.length + 1;
}
}
}
} else {
messageStart = 0;
}
if (messageStart == -1) {
console.debug(DEBUG_PREFIX+"message ignored, no trigger word preceding a message. Voice transcript: \""+ userMessageOriginal +"\"");
if (extension_settings.speech_recognition.Streaming.debug) {
toastr.info(
"No trigger word preceding a message. Voice transcript: \""+ userMessageOriginal +"\"",
DEBUG_PREFIX+"message ignored.",
{ timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true },
);
}
}
else{
userMessageFormatted = userMessageFormatted.substring(messageStart);
// Trim non alphanumeric character from the start
messageStart = 0;
for(const i of userMessageFormatted) {
if(/^[a-z]$/i.test(i)) {
break;
}
messageStart += 1;
}
userMessageFormatted = userMessageFormatted.substring(messageStart);
userMessageFormatted = userMessageFormatted.charAt(0).toUpperCase() + userMessageFormatted.substring(1);
processTranscript(userMessageFormatted);
}
}
else
{
console.debug(DEBUG_PREFIX+"Received empty transcript, ignored");
}
}
catch (error) {
console.debug(error);
}
finally {
inApiCall = false;
}
}
async function processTranscript(transcript) {
try {
const transcriptOriginal = transcript;
let transcriptFormatted = transcriptOriginal.trim();
if (transcriptFormatted.length > 0)
{
console.debug(DEBUG_PREFIX+"recorded transcript: \""+transcriptFormatted+"\"");
const messageMode = extension_settings.speech_recognition.messageMode;
console.debug(DEBUG_PREFIX+"mode: "+messageMode);
let transcriptLower = transcriptFormatted.toLowerCase()
// remove punctuation
let transcriptRaw = transcriptLower.replace(/[^\w\s\']|_/g, "").replace(/\s+/g, " ");
// Check message mapping
if (extension_settings.speech_recognition.messageMappingEnabled) {
console.debug(DEBUG_PREFIX+"Start searching message mapping into:",transcriptRaw)
for (const key in extension_settings.speech_recognition.messageMapping) {
console.debug(DEBUG_PREFIX+"message mapping searching: ", key,"=>",extension_settings.speech_recognition.messageMapping[key]);
if (transcriptRaw.includes(key)) {
var message = extension_settings.speech_recognition.messageMapping[key];
console.debug(DEBUG_PREFIX+"message mapping found: ", key,"=>",extension_settings.speech_recognition.messageMapping[key]);
$("#send_textarea").val(message);
if (messageMode == "auto_send") await getContext().generate();
return;
}
}
}
console.debug(DEBUG_PREFIX+"no message mapping found, processing transcript as normal message");
switch (messageMode) {
case "auto_send":
$('#send_textarea').val("") // clear message area to avoid double message
console.debug(DEBUG_PREFIX+"Sending message")
const context = getContext();
const messageText = transcriptFormatted;
const message = {
name: context.name1,
is_user: true,
send_date: getMessageTimeStamp(),
mes: messageText,
};
context.chat.push(message);
context.addOneMessage(message);
await context.generate();
$('#debug_output').text("<SST-module DEBUG>: message sent: \""+ transcriptFormatted +"\"");
break;
case "replace":
console.debug(DEBUG_PREFIX+"Replacing message")
$('#send_textarea').val(transcriptFormatted);
break;
case "append":
console.debug(DEBUG_PREFIX+"Appending message")
$('#send_textarea').val($('#send_textarea').val()+" "+transcriptFormatted);
break;
default:
console.debug(DEBUG_PREFIX+"Not supported stt message mode: "+messageMode)
}
}
else
{
console.debug(DEBUG_PREFIX+"Empty transcript, do nothing");
}
}
catch (error) {
console.debug(error);
}
}
function loadNavigatorAudioRecording() {
if (navigator.mediaDevices.getUserMedia) {
console.debug(DEBUG_PREFIX+' getUserMedia supported by browser.');
let onSuccess = function(stream) {
const mediaRecorder = new MediaRecorder(stream);
$("#microphone_button").off('click').on("click", function() {
if (!audioRecording) {
mediaRecorder.start();
console.debug(mediaRecorder.state);
console.debug("recorder started");
audioRecording = true;
$("#microphone_button").toggleClass('fa-microphone fa-microphone-slash');
}
else {
mediaRecorder.stop();
console.debug(mediaRecorder.state);
console.debug("recorder stopped");
audioRecording = false;
$("#microphone_button").toggleClass('fa-microphone fa-microphone-slash');
}
});
mediaRecorder.onstop = async function() {
console.debug(DEBUG_PREFIX+"data available after MediaRecorder.stop() called: ", audioChunks.length, " chunks");
const audioBlob = new Blob(audioChunks, { type: "audio/wav; codecs=0" });
audioChunks = [];
const transcript = await sttProvider.processAudio(audioBlob);
// TODO: lock and release recording while processing?
console.debug(DEBUG_PREFIX+"received transcript:", transcript);
processTranscript(transcript);
}
mediaRecorder.ondataavailable = function(e) {
audioChunks.push(e.data);
}
}
let onError = function(err) {
console.debug(DEBUG_PREFIX+"The following error occured: " + err);
}
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
} else {
console.debug(DEBUG_PREFIX+"getUserMedia not supported on your browser!");
toastr.error("getUserMedia not supported", DEBUG_PREFIX+"not supported for your browser.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
}
}
//##############//
// STT Provider //
//##############//
function loadSttProvider(provider) {
//Clear the current config and add new config
$("#speech_recognition_provider_settings").html("");
// Init provider references
extension_settings.speech_recognition.currentProvider = provider;
sttProviderName = provider;
if (!(sttProviderName in extension_settings.speech_recognition)) {
console.warn(`Provider ${sttProviderName} not in Extension Settings, initiatilizing provider in settings`);
extension_settings.speech_recognition[sttProviderName] = {};
}
$('#speech_recognition_provider').val(sttProviderName);
if (sttProviderName == "None") {
$("#microphone_button").hide();
$("#speech_recognition_message_mode_div").hide();
$("#speech_recognition_message_mapping_div").hide();
return;
}
$("#speech_recognition_message_mode_div").show();
$("#speech_recognition_message_mapping_div").show();
sttProvider = new sttProviders[sttProviderName]
// Init provider settings
$('#speech_recognition_provider_settings').append(sttProvider.settingsHtml);
// Use microphone button as push to talk
if (sttProviderName == "Browser") {
sttProvider.processTranscriptFunction = processTranscript;
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
$("#microphone_button").show();
}
if (sttProviderName == "Vosk" | sttProviderName == "Whisper") {
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
loadNavigatorAudioRecording();
$("#microphone_button").show();
}
if (sttProviderName == "Streaming") {
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
$("#microphone_button").off('click');
$("#microphone_button").hide();
}
}
function onSttProviderChange() {
const sttProviderSelection = $('#speech_recognition_provider').val();
loadSttProvider(sttProviderSelection);
saveSettingsDebounced();
}
function onSttProviderSettingsInput() {
sttProvider.onSettingsChange();
// Persist changes to SillyTavern stt extension settings
extension_settings.speech_recognition[sttProviderName] = sttProvider.settings;
saveSettingsDebounced();
console.info(`Saved settings ${sttProviderName} ${JSON.stringify(sttProvider.settings)}`);
}
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
currentProvider: "None",
messageMode: "append",
messageMappingText: "",
messageMapping: [],
messageMappingEnabled: false,
}
function loadSettings() {
if (Object.keys(extension_settings.speech_recognition).length === 0) {
Object.assign(extension_settings.speech_recognition, defaultSettings)
}
$('#speech_recognition_enabled').prop('checked',extension_settings.speech_recognition.enabled);
$('#speech_recognition_message_mode').val(extension_settings.speech_recognition.messageMode);
if (extension_settings.speech_recognition.messageMappingText.length > 0) {
$('#speech_recognition_message_mapping').val(extension_settings.speech_recognition.messageMappingText);
}
$('#speech_recognition_message_mapping_enabled').prop('checked',extension_settings.speech_recognition.messageMappingEnabled);
}
async function onMessageModeChange() {
extension_settings.speech_recognition.messageMode = $('#speech_recognition_message_mode').val();
if(sttProviderName != "Browser" & extension_settings.speech_recognition.messageMode == "auto_send") {
$("#speech_recognition_wait_response_div").show()
}
else {
$("#speech_recognition_wait_response_div").hide()
}
saveSettingsDebounced();
}
async function onMessageMappingChange() {
let array = $('#speech_recognition_message_mapping').val().split(",");
array = array.map(element => {return element.trim();});
array = array.filter((str) => str !== '');
extension_settings.speech_recognition.messageMapping = {};
for (const text of array) {
if (text.includes("=")) {
const pair = text.toLowerCase().split("=")
extension_settings.speech_recognition.messageMapping[pair[0].trim()] = pair[1].trim()
console.debug(DEBUG_PREFIX+"Added mapping", pair[0],"=>", extension_settings.speech_recognition.messageMapping[pair[0]]);
}
else {
console.debug(DEBUG_PREFIX+"Wrong syntax for message mapping, no '=' found in:", text);
}
}
$("#speech_recognition_message_mapping_status").text("Message mapping updated to: "+JSON.stringify(extension_settings.speech_recognition.messageMapping))
console.debug(DEBUG_PREFIX+"Updated message mapping", extension_settings.speech_recognition.messageMapping);
extension_settings.speech_recognition.messageMappingText = $('#speech_recognition_message_mapping').val()
saveSettingsDebounced();
}
async function onMessageMappingEnabledClick() {
extension_settings.speech_recognition.messageMappingEnabled = $('#speech_recognition_message_mapping_enabled').is(':checked');
saveSettingsDebounced()
}
$(document).ready(function () {
function addExtensionControls() {
const settingsHtml = `
<div id="speech_recognition_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Speech Recognition</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>
<span>Select Speech-to-text Provider</span> </br>
<select id="speech_recognition_provider">
</select>
</div>
<div id="speech_recognition_message_mode_div">
<span>Message Mode</span> </br>
<select id="speech_recognition_message_mode">
<option value="append">Append</option>
<option value="replace">Replace</option>
<option value="auto_send">Auto send</option>
</select>
</div>
<div id="speech_recognition_message_mapping_div">
<span>Message Mapping</span>
<textarea id="speech_recognition_message_mapping" class="text_pole textarea_compact" type="text" rows="4" placeholder="Enter comma separated phrases mapping, example:\ncommand delete = /del 2,\nslash delete = /del 2,\nsystem roll = /roll 2d6,\nhey continue = /continue"></textarea>
<span id="speech_recognition_message_mapping_status"></span>
<label class="checkbox_label" for="speech_recognition_message_mapping_enabled">
<input type="checkbox" id="speech_recognition_message_mapping_enabled" name="speech_recognition_message_mapping_enabled">
<small>Enable messages mapping</small>
</label>
</div>
<form id="speech_recognition_provider_settings" class="inline-drawer-content">
</form>
</div>
</div>
</div>
`;
$('#extensions_settings').append(settingsHtml);
$('#speech_recognition_provider_settings').on('input', onSttProviderSettingsInput);
for (const provider in sttProviders) {
$('#speech_recognition_provider').append($("<option />").val(provider).text(provider));
console.debug(DEBUG_PREFIX+"added option "+provider);
}
$('#speech_recognition_provider').on('change', onSttProviderChange);
$('#speech_recognition_message_mode').on('change', onMessageModeChange);
$('#speech_recognition_message_mapping').on('change', onMessageMappingChange);
$('#speech_recognition_message_mapping_enabled').on('click', onMessageMappingEnabledClick);
const $button = $('<div id="microphone_button" class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
$('#send_but_sheld').prepend($button);
}
addExtensionControls(); // No init dependencies
loadSettings(); // Depends on Extension Controls and loadTtsProvider
loadSttProvider(extension_settings.speech_recognition.currentProvider); // No dependencies
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
moduleWorker();
})

View File

@ -1,14 +0,0 @@
{
"display_name": "Speech Recognition",
"loading_order": 13,
"requires": [],
"optional": [
"vosk-speech-recognition",
"whisper-speech-recognition"
],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207 and Keij#6799",
"version": "1.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,109 +0,0 @@
import { getApiUrl, doExtrasFetch, modules } from "../../extensions.js";
export { StreamingSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (streaming)> "
class StreamingSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
triggerWordsText: "",
triggerWords : [],
triggerWordsEnabled : false,
triggerWordsIncluded: false,
debug : false,
}
get settingsHtml() {
let html = '\
<div id="speech_recognition_streaming_trigger_words_div">\
<span>Trigger words</span>\
<textarea id="speech_recognition_streaming_trigger_words" class="text_pole textarea_compact" type="text" rows="4" placeholder="Enter comma separated words that triggers new message, example:\nhey, hey aqua, record, listen"></textarea>\
<label class="checkbox_label" for="speech_recognition_streaming_trigger_words_enabled">\
<input type="checkbox" id="speech_recognition_streaming_trigger_words_enabled" name="speech_recognition_trigger_words_enabled">\
<small>Enable trigger words</small>\
</label>\
<label class="checkbox_label" for="speech_recognition_trigger_words_included">\
<input type="checkbox" id="speech_recognition_trigger_words_included" name="speech_recognition_trigger_words_included">\
<small>Include trigger words in message</small>\
</label>\
<label class="checkbox_label" for="speech_recognition_streaming_debug">\
<input type="checkbox" id="speech_recognition_streaming_debug" name="speech_recognition_streaming_debug">\
<small>Enable debug pop ups</small>\
</label>\
</div>\
'
return html
}
onSettingsChange() {
this.settings.triggerWordsText = $('#speech_recognition_streaming_trigger_words').val();
let array = $('#speech_recognition_streaming_trigger_words').val().split(",");
array = array.map(element => {return element.trim().toLowerCase();});
array = array.filter((str) => str !== '');
this.settings.triggerWords = array;
this.settings.triggerWordsEnabled = $("#speech_recognition_streaming_trigger_words_enabled").is(':checked');
this.settings.triggerWordsIncluded = $("#speech_recognition_trigger_words_included").is(':checked');
this.settings.debug = $("#speech_recognition_streaming_debug").is(':checked');
console.debug(DEBUG_PREFIX+" Updated settings: ", this.settings);
this.loadSettings(this.settings);
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default Whisper STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
$("#speech_recognition_streaming_trigger_words").val(this.settings.triggerWordsText);
$("#speech_recognition_streaming_trigger_words_enabled").prop('checked',this.settings.triggerWordsEnabled);
$("#speech_recognition_trigger_words_included").prop('checked',this.settings.triggerWordsIncluded);
$("#speech_recognition_streaming_debug").prop('checked',this.settings.debug);
console.debug(DEBUG_PREFIX+"streaming STT settings loaded")
}
async getUserMessage() {
// Return if module is not loaded
if (!modules.includes('streaming-stt')) {
console.debug(DEBUG_PREFIX+"Module streaming-stt must be activated in Sillytavern Extras for streaming user voice.")
return "";
}
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/streaming/record-and-transcript';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: "" }),
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX+'STT Generation Failed (streaming)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const data = await apiResult.json();
return data.transcript;
}
}

View File

@ -1,3 +0,0 @@
.speech-toggle {
display: flex;
}

View File

@ -1,65 +0,0 @@
import { getApiUrl, doExtrasFetch } from "../../extensions.js";
export { VoskSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Vosk)> "
class VoskSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
}
get settingsHtml() {
let html = ""
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default vosk STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
console.debug(DEBUG_PREFIX+"Vosk STT settings loaded")
}
async processAudio(audioblob) {
var requestData = new FormData();
requestData.append('AudioFile', audioblob, 'record.wav');
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/vosk/process-audio';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
body: requestData,
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, 'STT Generation Failed (Vosk)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const result = await apiResult.json();
return result.transcript;
}
}

View File

@ -1,67 +0,0 @@
import { getApiUrl, doExtrasFetch } from "../../extensions.js";
export { WhisperSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Vosk)> "
class WhisperSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
//model_path: "",
}
get settingsHtml() {
let html = ""
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default Whisper STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
console.debug(DEBUG_PREFIX+"Whisper STT settings loaded")
}
async processAudio(audioblob) {
var requestData = new FormData();
requestData.append('AudioFile', audioblob, 'record.wav');
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/whisper/process-audio';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
body: requestData,
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, 'STT Generation Failed (Whisper)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const result = await apiResult.json();
return result.transcript;
}
}

View File

@ -121,6 +121,17 @@ const helpString = [
example: '/sd apple tree' would generate a picture of an apple tree.`, example: '/sd apple tree' would generate a picture of an apple tree.`,
].join('<br>'); ].join('<br>');
const defaultPrefix = 'best quality, absurdres, aesthetic,';
const defaultNegative = 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry';
const defaultStyles = [
{
name: 'Default',
negative: defaultNegative,
prefix: defaultPrefix,
},
];
const defaultSettings = { const defaultSettings = {
source: sources.extras, source: sources.extras,
@ -143,8 +154,8 @@ const defaultSettings = {
width: 512, width: 512,
height: 512, height: 512,
prompt_prefix: 'best quality, absurdres, masterpiece,', prompt_prefix: defaultPrefix,
negative_prompt: 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry', negative_prompt: defaultNegative,
sampler: 'DDIM', sampler: 'DDIM',
model: '', model: '',
@ -156,9 +167,11 @@ const defaultSettings = {
horde: false, horde: false,
horde_nsfw: false, horde_nsfw: false,
horde_karras: true, horde_karras: true,
horde_sanitize: true,
// Refine mode // Refine mode
refine_mode: false, refine_mode: false,
expand: false,
prompts: promptTemplates, prompts: promptTemplates,
@ -189,6 +202,9 @@ const defaultSettings = {
novel_upscale_ratio_step: 0.1, novel_upscale_ratio_step: 0.1,
novel_upscale_ratio: 1.0, novel_upscale_ratio: 1.0,
novel_anlas_guard: false, novel_anlas_guard: false,
style: 'Default',
styles: defaultStyles,
} }
function getSdRequestBody() { function getSdRequestBody() {
@ -237,6 +253,10 @@ async function loadSettings() {
extension_settings.sd.character_prompts = {}; extension_settings.sd.character_prompts = {};
} }
if (!Array.isArray(extension_settings.sd.styles)) {
extension_settings.sd.styles = defaultStyles;
}
$('#sd_source').val(extension_settings.sd.source); $('#sd_source').val(extension_settings.sd.source);
$('#sd_scale').val(extension_settings.sd.scale).trigger('input'); $('#sd_scale').val(extension_settings.sd.scale).trigger('input');
$('#sd_steps').val(extension_settings.sd.steps).trigger('input'); $('#sd_steps').val(extension_settings.sd.steps).trigger('input');
@ -252,14 +272,24 @@ async function loadSettings() {
$('#sd_horde').prop('checked', extension_settings.sd.horde); $('#sd_horde').prop('checked', extension_settings.sd.horde);
$('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw); $('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw);
$('#sd_horde_karras').prop('checked', extension_settings.sd.horde_karras); $('#sd_horde_karras').prop('checked', extension_settings.sd.horde_karras);
$('#sd_horde_sanitize').prop('checked', extension_settings.sd.horde_sanitize);
$('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces); $('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr); $('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode); $('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
$('#sd_expand').prop('checked', extension_settings.sd.expand);
$('#sd_auto_url').val(extension_settings.sd.auto_url); $('#sd_auto_url').val(extension_settings.sd.auto_url);
$('#sd_auto_auth').val(extension_settings.sd.auto_auth); $('#sd_auto_auth').val(extension_settings.sd.auto_auth);
$('#sd_vlad_url').val(extension_settings.sd.vlad_url); $('#sd_vlad_url').val(extension_settings.sd.vlad_url);
$('#sd_vlad_auth').val(extension_settings.sd.vlad_auth); $('#sd_vlad_auth').val(extension_settings.sd.vlad_auth);
for (const style of extension_settings.sd.styles) {
const option = document.createElement('option');
option.value = style.name;
option.text = style.name;
option.selected = style.name === extension_settings.sd.style;
$('#sd_style').append(option);
}
toggleSourceControls(); toggleSourceControls();
addPromptTemplates(); addPromptTemplates();
@ -298,7 +328,88 @@ function addPromptTemplates() {
} }
} }
async function refinePrompt(prompt) { function onStyleSelect() {
const selectedStyle = String($('#sd_style').find(':selected').val());
const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
if (!styleObject) {
console.warn(`Could not find style object for ${selectedStyle}`);
return;
}
$('#sd_prompt_prefix').val(styleObject.prefix).trigger('input');
$('#sd_negative_prompt').val(styleObject.negative).trigger('input');
extension_settings.sd.style = selectedStyle;
saveSettingsDebounced();
}
async function onSaveStyleClick() {
const userInput = await callPopup('Enter style name:', 'input', '', { okButton: 'Save' });
if (!userInput) {
return;
}
const name = String(userInput).trim();
const prefix = String($('#sd_prompt_prefix').val());
const negative = String($('#sd_negative_prompt').val());
const existingStyle = extension_settings.sd.styles.find(x => x.name === name);
if (existingStyle) {
existingStyle.prefix = prefix;
existingStyle.negative = negative;
$('#sd_style').val(name);
saveSettingsDebounced();
return;
}
const styleObject = {
name: name,
prefix: prefix,
negative: negative,
};
extension_settings.sd.styles.push(styleObject);
const option = document.createElement('option');
option.value = styleObject.name;
option.text = styleObject.name;
option.selected = true;
$('#sd_style').append(option);
$('#sd_style').val(styleObject.name);
saveSettingsDebounced();
}
async function expandPrompt(prompt) {
try {
const response = await fetch('/api/sd/expand', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ prompt: prompt }),
});
if (!response.ok) {
throw new Error('API returned an error.');
}
const data = await response.json();
return data.prompt;
} catch {
return prompt;
}
}
/**
* Modifies prompt based on auto-expansion and user inputs.
* @param {string} prompt Prompt to refine
* @param {boolean} allowExpand Whether to allow auto-expansion
* @returns {Promise<string>} Refined prompt
*/
async function refinePrompt(prompt, allowExpand) {
if (allowExpand && extension_settings.sd.expand) {
prompt = await expandPrompt(prompt);
}
if (extension_settings.sd.refine_mode) { if (extension_settings.sd.refine_mode) {
const refinedPrompt = await callPopup('<h3>Review and edit the prompt:</h3>Press "Cancel" to abort the image generation.', 'input', prompt.trim(), { rows: 5, okButton: 'Generate' }); const refinedPrompt = await callPopup('<h3>Review and edit the prompt:</h3>Press "Cancel" to abort the image generation.', 'input', prompt.trim(), { rows: 5, okButton: 'Generate' });
@ -344,7 +455,14 @@ function getCharacterPrefix() {
return ''; return '';
} }
function combinePrefixes(str1, str2) { /**
* Combines two prompt prefixes into one.
* @param {string} str1 Base string
* @param {string} str2 Secondary string
* @param {string} macro Macro to replace with the secondary string
* @returns {string} Combined string with a comma between them
*/
function combinePrefixes(str1, str2, macro = '') {
if (!str2) { if (!str2) {
return str1; return str1;
} }
@ -353,12 +471,16 @@ function combinePrefixes(str1, str2) {
str1 = str1.trim().replace(/^,|,$/g, ''); str1 = str1.trim().replace(/^,|,$/g, '');
str2 = str2.trim().replace(/^,|,$/g, ''); str2 = str2.trim().replace(/^,|,$/g, '');
// Combine the strings with a comma between them // Combine the strings with a comma between them)
var result = `${str1}, ${str2},`; const result = macro && str1.includes(macro) ? str1.replace(macro, str2) : `${str1}, ${str2},`;
return result; return result;
} }
function onExpandInput() {
extension_settings.sd.expand = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onRefineModeInput() { function onRefineModeInput() {
extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked'); extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
@ -439,16 +561,21 @@ function onNovelAnlasGuardInput() {
saveSettingsDebounced(); saveSettingsDebounced();
} }
async function onHordeNsfwInput() { function onHordeNsfwInput() {
extension_settings.sd.horde_nsfw = !!$(this).prop('checked'); extension_settings.sd.horde_nsfw = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
} }
async function onHordeKarrasInput() { function onHordeKarrasInput() {
extension_settings.sd.horde_karras = !!$(this).prop('checked'); extension_settings.sd.horde_karras = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onHordeSanitizeInput() {
extension_settings.sd.horde_sanitize = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onRestoreFacesInput() { function onRestoreFacesInput() {
extension_settings.sd.restore_faces = !!$(this).prop('checked'); extension_settings.sd.restore_faces = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
@ -954,17 +1081,21 @@ async function loadNovelModels() {
} }
return [ return [
{
value: 'nai-diffusion-2',
text: 'NAI Diffusion Anime V2',
},
{ {
value: 'nai-diffusion', value: 'nai-diffusion',
text: 'Full', text: 'NAI Diffusion Anime V1 (Full)',
}, },
{ {
value: 'safe-diffusion', value: 'safe-diffusion',
text: 'Safe', text: 'NAI Diffusion Anime V1 (Curated)',
}, },
{ {
value: 'nai-diffusion-furry', value: 'nai-diffusion-furry',
text: 'Furry', text: 'NAI Diffusion Furry',
}, },
]; ];
} }
@ -1073,10 +1204,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);
@ -1122,14 +1252,14 @@ async function getPrompt(generationType, message, trigger, quiet_prompt) {
} }
if (generationType !== generationMode.FREE) { if (generationType !== generationMode.FREE) {
prompt = await refinePrompt(prompt); prompt = await refinePrompt(prompt, true);
} }
return prompt; return prompt;
} }
async function generatePrompt(quiet_prompt) { async function generatePrompt(quiet_prompt) {
const reply = await generateQuietPrompt(quiet_prompt, false); const reply = await generateQuietPrompt(quiet_prompt, false, false);
return processReply(reply); return processReply(reply);
} }
@ -1138,7 +1268,7 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix()) ? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix())
: extension_settings.sd.prompt_prefix; : extension_settings.sd.prompt_prefix;
const prefixedPrompt = combinePrefixes(prefix, prompt); const prefixedPrompt = combinePrefixes(prefix, prompt, '{prompt}');
let result = { format: '', data: '' }; let result = { format: '', data: '' };
const currentChatId = getCurrentChatId(); const currentChatId = getCurrentChatId();
@ -1243,6 +1373,7 @@ async function generateHordeImage(prompt) {
nsfw: extension_settings.sd.horde_nsfw, nsfw: extension_settings.sd.horde_nsfw,
restore_faces: !!extension_settings.sd.restore_faces, restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr, enable_hr: !!extension_settings.sd.enable_hr,
sanitize: !!extension_settings.sd.horde_sanitize,
}), }),
}); });
@ -1337,13 +1468,13 @@ function getNovelParams() {
let width = extension_settings.sd.width; let width = extension_settings.sd.width;
let height = extension_settings.sd.height; let height = extension_settings.sd.height;
// Don't apply Anlas guard if it's disabled.d // Don't apply Anlas guard if it's disabled.
if (!extension_settings.sd.novel_anlas_guard) { if (!extension_settings.sd.novel_anlas_guard) {
return { steps, width, height }; return { steps, width, height };
} }
const MAX_STEPS = 28; const MAX_STEPS = 28;
const MAX_PIXELS = 409600; const MAX_PIXELS = 1024 * 1024;
if (width * height > MAX_PIXELS) { if (width * height > MAX_PIXELS) {
const ratio = Math.sqrt(MAX_PIXELS / (width * height)); const ratio = Math.sqrt(MAX_PIXELS / (width * height));
@ -1515,7 +1646,7 @@ async function sdMessageButton(e) {
try { try {
setBusyIcon(true); setBusyIcon(true);
if (hasSavedImage) { if (hasSavedImage) {
const prompt = await refinePrompt(message.extra.title); const prompt = await refinePrompt(message.extra.title, false);
message.extra.title = prompt; message.extra.title = prompt;
console.log('Regenerating an image, using existing prompt:', prompt); console.log('Regenerating an image, using existing prompt:', prompt);
@ -1584,6 +1715,7 @@ jQuery(async () => {
$('#sd_height').on('input', onHeightInput); $('#sd_height').on('input', onHeightInput);
$('#sd_horde_nsfw').on('input', onHordeNsfwInput); $('#sd_horde_nsfw').on('input', onHordeNsfwInput);
$('#sd_horde_karras').on('input', onHordeKarrasInput); $('#sd_horde_karras').on('input', onHordeKarrasInput);
$('#sd_horde_sanitize').on('input', onHordeSanitizeInput);
$('#sd_restore_faces').on('input', onRestoreFacesInput); $('#sd_restore_faces').on('input', onRestoreFacesInput);
$('#sd_enable_hr').on('input', onHighResFixInput); $('#sd_enable_hr').on('input', onHighResFixInput);
$('#sd_refine_mode').on('input', onRefineModeInput); $('#sd_refine_mode').on('input', onRefineModeInput);
@ -1601,6 +1733,9 @@ jQuery(async () => {
$('#sd_novel_upscale_ratio').on('input', onNovelUpscaleRatioInput); $('#sd_novel_upscale_ratio').on('input', onNovelUpscaleRatioInput);
$('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput); $('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput);
$('#sd_novel_view_anlas').on('click', onViewAnlasClick); $('#sd_novel_view_anlas').on('click', onViewAnlasClick);
$('#sd_expand').on('input', onExpandInput);
$('#sd_style').on('change', onStyleSelect);
$('#sd_save_style').on('click', onSaveStyleClick);
$('#sd_character_prompt_block').hide(); $('#sd_character_prompt_block').hide();
$('.sd_settings .inline-drawer-toggle').on('click', function () { $('.sd_settings .inline-drawer-toggle').on('click', function () {

View File

@ -12,6 +12,14 @@
<input id="sd_refine_mode" type="checkbox" /> <input id="sd_refine_mode" type="checkbox" />
Edit prompts before generation Edit prompts before generation
</label> </label>
<label for="sd_expand" class="checkbox_label" title="Automatically extend prompts using text generation model">
<input id="sd_expand" type="checkbox" />
Auto-enhance prompts
</label>
<small>
This option uses an additional GPT-2 text generation model to add more details to the prompt generated by the main API.
Works best for SDXL image models. May not work well with other models, it is recommended to manually edit prompts in this case.
</small>
<label for="sd_source">Source</label> <label for="sd_source">Source</label>
<select id="sd_source"> <select id="sd_source">
<option value="extras">Extras API (local / remote)</option> <option value="extras">Extras API (local / remote)</option>
@ -58,6 +66,12 @@
Allow NSFW images from Horde Allow NSFW images from Horde
</span> </span>
</label> </label>
<label for="sd_horde_sanitize" class="checkbox_label">
<input id="sd_horde_sanitize" type="checkbox" />
<span data-i18n="Sanitize prompts (recommended)">
Sanitize prompts (recommended)
</span>
</label>
<label for="sd_horde_karras" class="checkbox_label"> <label for="sd_horde_karras" class="checkbox_label">
<input id="sd_horde_karras" type="checkbox" /> <input id="sd_horde_karras" type="checkbox" />
<span data-i18n="Karras (not all samplers supported)"> <span data-i18n="Karras (not all samplers supported)">
@ -116,15 +130,25 @@
<label for="sd_novel_upscale_ratio">Upscale by (<span id="sd_novel_upscale_ratio_value"></span>)</label> <label for="sd_novel_upscale_ratio">Upscale by (<span id="sd_novel_upscale_ratio_value"></span>)</label>
<input id="sd_novel_upscale_ratio" type="range" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" /> <input id="sd_novel_upscale_ratio" type="range" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" />
</div> </div>
<hr>
<h4 title="Preset for prompt prefix and negative prompt">
Style
</h4>
<div class="flex-container">
<select id="sd_style" class="flex1 text_pole"></select>
<div id="sd_save_style" title="Save style" class="menu_button">
<i class="fa-solid fa-save"></i>
</div>
</div>
<label for="sd_prompt_prefix">Common prompt prefix</label> <label for="sd_prompt_prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3"></textarea> <textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
<label for="sd_negative_prompt">Negative prompt</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea>
<div id="sd_character_prompt_block"> <div id="sd_character_prompt_block">
<label for="sd_character_prompt">Character-specific prompt prefix</label> <label for="sd_character_prompt">Character-specific prompt prefix</label>
<small>Won't be used in groups.</small> <small>Won't be used in groups.</small>
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea> <textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea>
</div> </div>
<label for="sd_negative_prompt">Negative prompt</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea>
</div> </div>
</div> </div>
<div class="inline-drawer"> <div class="inline-drawer">

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

@ -1,3 +1,5 @@
export {translate};
import { import {
callPopup, callPopup,
eventSource, eventSource,
@ -11,7 +13,7 @@ import {
import { extension_settings, getContext } from "../../extensions.js"; import { extension_settings, getContext } from "../../extensions.js";
import { secret_state, writeSecret } from "../../secrets.js"; import { secret_state, writeSecret } from "../../secrets.js";
const autoModeOptions = { export const autoModeOptions = {
NONE: 'none', NONE: 'none',
RESPONSES: 'responses', RESPONSES: 'responses',
INPUT: 'inputs', INPUT: 'inputs',

View File

@ -10,15 +10,12 @@ class ElevenLabsTtsProvider {
voices = [] voices = []
separator = ' ... ... ... ' separator = ' ... ... ... '
get settings() {
return this.settings
}
defaultSettings = { defaultSettings = {
stability: 0.75, stability: 0.75,
similarity_boost: 0.75, similarity_boost: 0.75,
apiKey: "", apiKey: "",
multilingual: false, model: 'eleven_monolingual_v1',
voiceMap: {} voiceMap: {}
} }
@ -27,15 +24,17 @@ class ElevenLabsTtsProvider {
<div class="elevenlabs_tts_settings"> <div class="elevenlabs_tts_settings">
<label for="elevenlabs_tts_api_key">API Key</label> <label for="elevenlabs_tts_api_key">API Key</label>
<input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/> <input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>
<label for="elevenlabs_tts_model">Model</label>
<select id="elevenlabs_tts_model" class="text_pole">
<option value="eleven_monolingual_v1">Monolingual</option>
<option value="eleven_multilingual_v1">Multilingual v1</option>
<option value="eleven_multilingual_v2">Multilingual v2</option>
</select>
<input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" /> <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
<label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label> <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
<input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.05" /> <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.05" />
<label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label> <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
<input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.05" /> <input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.05" />
<label class="checkbox_label" for="elevenlabs_tts_multilingual">
<input id="elevenlabs_tts_multilingual" type="checkbox" value="${this.defaultSettings.multilingual}" />
Enable Multilingual
</label>
</div> </div>
` `
return html return html
@ -45,11 +44,10 @@ class ElevenLabsTtsProvider {
// Update dynamically // Update dynamically
this.settings.stability = $('#elevenlabs_tts_stability').val() this.settings.stability = $('#elevenlabs_tts_stability').val()
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val() this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val()
this.settings.multilingual = $('#elevenlabs_tts_multilingual').prop('checked') this.settings.model = $('#elevenlabs_tts_model').find(':selected').val()
saveTtsProviderSettings() saveTtsProviderSettings()
} }
async loadSettings(settings) { async loadSettings(settings) {
// Pupulate Provider UI given input settings // Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) { if (Object.keys(settings).length == 0) {
@ -59,26 +57,39 @@ class ElevenLabsTtsProvider {
// Only accept keys defined in defaultSettings // Only accept keys defined in defaultSettings
this.settings = this.defaultSettings this.settings = this.defaultSettings
for (const key in settings){ // Migrate old settings
if (key in this.settings){ if (settings['multilingual'] !== undefined) {
settings.model = settings.multilingual ? 'eleven_multilingual_v1' : 'eleven_monolingual_v1';
delete settings['multilingual'];
}
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key] this.settings[key] = settings[key]
} else { } else {
throw `Invalid setting passed to TTS Provider: ${key}` throw `Invalid setting passed to TTS Provider: ${key}`
} }
} }
$('#elevenlabs_tts_stability').val(this.settings.stability) $('#elevenlabs_tts_stability').val(this.settings.stability)
$('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost) $('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost)
$('#elevenlabs_tts_api_key').val(this.settings.apiKey) $('#elevenlabs_tts_api_key').val(this.settings.apiKey)
$('#elevenlabs_tts_multilingual').prop('checked', this.settings.multilingual) $('#elevenlabs_tts_model').val(this.settings.model);
$('#eleven_labs_connect').on('click', () => {this.onConnectClick()}) $('#eleven_labs_connect').on('click', () => { this.onConnectClick() })
$('#elevenlabs_tts_settings').on('input',this.onSettingsChange) $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this))
await this.checkReady() try {
console.debug("ElevenLabs: Settings loaded") await this.checkReady()
console.debug("ElevenLabs: Settings loaded")
} catch {
console.debug("ElevenLabs: Settings loaded, but not ready")
}
} }
// Perform a simple readiness check by trying to fetch voiceIds // Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){ async checkReady() {
await this.fetchTtsVoiceObjects() await this.fetchTtsVoiceObjects()
} }
@ -87,7 +98,7 @@ class ElevenLabsTtsProvider {
async onConnectClick() { async onConnectClick() {
// Update on Apply click // Update on Apply click
return await this.updateApiKey().catch( (error) => { return await this.updateApiKey().catch((error) => {
toastr.error(`ElevenLabs: ${error}`) toastr.error(`ElevenLabs: ${error}`)
}) })
} }
@ -102,6 +113,7 @@ class ElevenLabsTtsProvider {
}) })
this.settings.apiKey = this.settings.apiKey this.settings.apiKey = this.settings.apiKey
console.debug(`Saved new API_KEY: ${this.settings.apiKey}`) console.debug(`Saved new API_KEY: ${this.settings.apiKey}`)
$('#tts_status').text('')
this.onSettingsChange() this.onSettingsChange()
} }
@ -123,7 +135,7 @@ class ElevenLabsTtsProvider {
} }
async generateTts(text, voiceId){ async generateTts(text, voiceId) {
const historyId = await this.findTtsGenerationInHistory(text, voiceId) const historyId = await this.findTtsGenerationInHistory(text, voiceId)
let response let response
@ -189,11 +201,8 @@ class ElevenLabsTtsProvider {
} }
async fetchTtsGeneration(text, voiceId) { async fetchTtsGeneration(text, voiceId) {
let model = "eleven_monolingual_v1" let model = this.settings.model ?? "eleven_monolingual_v1";
if (this.settings.multilingual == true) { console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`)
model = "eleven_multilingual_v1"
}
console.info(`Generating new TTS for voice_id ${voiceId}`)
const response = await fetch( const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{ {
@ -203,9 +212,12 @@ class ElevenLabsTtsProvider {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
model: model, model_id: model,
text: text, text: text,
voice_settings: this.settings voice_settings: {
stability: Number(this.settings.stability),
similarity_boost: Number(this.settings.similarity_boost),
},
}) })
} }
) )

View File

@ -8,7 +8,6 @@ import { CoquiTtsProvider } from './coqui.js'
import { SystemTtsProvider } from './system.js' import { SystemTtsProvider } from './system.js'
import { NovelTtsProvider } from './novel.js' import { NovelTtsProvider } from './novel.js'
import { power_user } from '../../power-user.js' import { power_user } from '../../power-user.js'
import { rvcVoiceConversion } from "../rvc/index.js"
export { talkingAnimation }; export { talkingAnimation };
const UPDATE_INTERVAL = 1000 const UPDATE_INTERVAL = 1000
@ -415,8 +414,8 @@ async function tts(text, voiceId, char) {
let response = await ttsProvider.generateTts(text, voiceId) let response = await ttsProvider.generateTts(text, voiceId)
// RVC injection // RVC injection
if (extension_settings.rvc.enabled) if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function')
response = await rvcVoiceConversion(response, char, text) response = await window['rvcVoiceConversion'](response, char, text)
addAudioJob(response) addAudioJob(response)
completeTtsJob() completeTtsJob()
@ -430,7 +429,7 @@ async function processTtsQueue() {
console.debug('New message found, running TTS') console.debug('New message found, running TTS')
currentTtsJob = ttsJobQueue.shift() currentTtsJob = ttsJobQueue.shift()
let text = extension_settings.tts.narrate_translated_only ? currentTtsJob?.extra?.display_text : currentTtsJob.mes let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes
text = extension_settings.tts.narrate_dialogues_only text = extension_settings.tts.narrate_dialogues_only
? text.replace(/\*[^\*]*?(\*|$)/g, '').trim() // remove asterisks content ? text.replace(/\*[^\*]*?(\*|$)/g, '').trim() // remove asterisks content
: text.replaceAll('*', '').trim() // remove just the asterisks : text.replaceAll('*', '').trim() // remove just the asterisks
@ -615,8 +614,8 @@ function onTtsProviderChange() {
// Ensure that TTS provider settings are saved to extension settings. // Ensure that TTS provider settings are saved to extension settings.
export function saveTtsProviderSettings() { export function saveTtsProviderSettings() {
updateVoiceMap()
extension_settings.tts[ttsProviderName] = ttsProvider.settings extension_settings.tts[ttsProviderName] = ttsProvider.settings
updateVoiceMap()
saveSettingsDebounced() saveSettingsDebounced()
console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`) console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`)
} }
@ -696,6 +695,9 @@ function updateVoiceMap() {
voiceMap = tempVoiceMap voiceMap = tempVoiceMap
console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`) console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`)
} }
if (!extension_settings.tts[ttsProviderName].voiceMap) {
extension_settings.tts[ttsProviderName].voiceMap = {}
}
Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap) Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap)
saveSettingsDebounced() saveSettingsDebounced()
} }

View File

@ -1,66 +0,0 @@
import { getContext } from "../../extensions.js";
/**
* Gets a chat variable from the current chat metadata.
* @param {string} name The name of the variable to get.
* @returns {string} The value of the variable.
*/
function getChatVariable(name) {
const metadata = getContext().chatMetadata;
if (!metadata) {
return '';
}
if (!metadata.variables) {
metadata.variables = {};
return '';
}
return metadata.variables[name] || '';
}
/**
* Sets a chat variable in the current chat metadata.
* @param {string} name The name of the variable to set.
* @param {any} value The value of the variable to set.
*/
function setChatVariable(name, value) {
if (name === undefined || value === undefined) {
return;
}
const metadata = getContext().chatMetadata;
if (!metadata) {
return;
}
if (!metadata.variables) {
metadata.variables = {};
}
metadata.variables[name] = value;
}
function listChatVariables() {
const metadata = getContext().chatMetadata;
if (!metadata) {
return '';
}
if (!metadata.variables) {
metadata.variables = {};
return '';
}
return Object.keys(metadata.variables).map(key => `${key}=${metadata.variables[key]}`).join(';');
}
jQuery(() => {
const context = getContext();
context.registerHelper('getvar', getChatVariable);
context.registerHelper('setvar', setChatVariable);
context.registerHelper('listvar', listChatVariables);
});

View File

@ -1,11 +0,0 @@
{
"display_name": "Chat Variables",
"loading_order": 100,
"requires": [],
"optional": [],
"js": "index.js",
"css": "",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -66,6 +66,8 @@ import {
system_avatar, system_avatar,
isChatSaving, isChatSaving,
setExternalAbortController, setExternalAbortController,
baseChatReplace,
depth_prompt_depth_default,
} from "../script.js"; } from "../script.js";
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map, printTagFilters } from './tags.js'; import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map, printTagFilters } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js'; import { FILTER_TYPES, FilterHelper } from './filters.js';
@ -101,6 +103,11 @@ export const group_activation_strategy = {
LIST: 1, LIST: 1,
}; };
export const group_generation_mode = {
SWAP: 0,
APPEND: 1,
}
export const groupCandidatesFilter = new FilterHelper(debounce(printGroupCandidates, 100)); export const groupCandidatesFilter = new FilterHelper(debounce(printGroupCandidates, 100));
const groupAutoModeInterval = setInterval(groupChatAutoModeWorker, 5000); const groupAutoModeInterval = setInterval(groupChatAutoModeWorker, 5000);
const saveGroupDebounced = debounce(async (group, reload) => await _save(group, reload), 500); const saveGroupDebounced = debounce(async (group, reload) => await _save(group, reload), 500);
@ -193,6 +200,104 @@ export async function getGroupChat(groupId) {
eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
} }
/**
* Gets depth prompts for group members.
* @param {string} groupId Group ID
* @param {number} characterId Current Character ID
* @returns {{depth: number, text: string}[]} Array of depth prompts
*/
export function getGroupDepthPrompts(groupId, characterId) {
if (!groupId) {
return [];
}
console.debug('getGroupDepthPrompts entered for group: ', groupId);
const group = groups.find(x => x.id === groupId);
if (!group || !Array.isArray(group.members) || !group.members.length) {
return [];
}
if (group.generation_mode === group_generation_mode.SWAP) {
return [];
}
const depthPrompts = [];
for (const member of group.members) {
const index = characters.findIndex(x => x.avatar === member);
const character = characters[index];
if (index === -1 || !character) {
console.debug(`Skipping missing member: ${member}`);
continue;
}
if (group.disabled_members.includes(member) && characterId !== index) {
console.debug(`Skipping disabled group member: ${member}`);
continue;
}
const depthPromptText = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, character.name) || '';
const depthPromptDepth = character.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
if (depthPromptText) {
depthPrompts.push({ text: depthPromptText, depth: depthPromptDepth });
}
}
return depthPrompts;
}
/**
* Combines group members info a single string. Only for groups with generation mode set to APPEND.
* @param {string} groupId Group ID
* @param {number} characterId Current Character ID
* @returns {{description: string, personality: string, scenario: string, mesExample: string}} Group character cards combined
*/
export function getGroupCharacterCards(groupId, characterId) {
console.debug('getGroupCharacterCards entered for group: ', groupId);
const group = groups.find(x => x.id === groupId);
if (!group || group?.generation_mode !== group_generation_mode.APPEND || !Array.isArray(group.members) || !group.members.length) {
return null;
}
const scenarioOverride = chat_metadata['scenario'];
let descriptions = [];
let personalities = [];
let scenarios = [];
let mesExamples = [];
for (const member of group.members) {
const index = characters.findIndex(x => x.avatar === member);
const character = characters[index];
if (index === -1 || !character) {
console.debug(`Skipping missing member: ${member}`);
continue;
}
if (group.disabled_members.includes(member) && characterId !== index) {
console.debug(`Skipping disabled group member: ${member}`);
continue;
}
descriptions.push(baseChatReplace(character.description.trim(), name1, character.name));
personalities.push(baseChatReplace(character.personality.trim(), name1, character.name));
scenarios.push(baseChatReplace(character.scenario.trim(), name1, character.name));
mesExamples.push(baseChatReplace(character.mes_example.trim(), name1, character.name));
}
const description = descriptions.join('\n');
const personality = personalities.join('\n');
const scenario = scenarioOverride?.trim() || scenarios.join('\n');
const mesExample = mesExamples.join('\n');
return { description, personality, scenario, mesExample };
}
function getFirstCharacterMessage(character) { function getFirstCharacterMessage(character) {
let messageText = character.first_mes; let messageText = character.first_mes;
@ -922,6 +1027,14 @@ async function onGroupActivationStrategyInput(e) {
} }
} }
async function onGroupGenerationModeInput(e) {
if (openGroupId) {
let _thisGroup = groups.find((x) => x.id == openGroupId);
_thisGroup.generation_mode = Number(e.target.value);
await editGroup(openGroupId, false, false);
}
}
async function onGroupNameInput() { async function onGroupNameInput() {
if (openGroupId) { if (openGroupId) {
let _thisGroup = groups.find((x) => x.id == openGroupId); let _thisGroup = groups.find((x) => x.id == openGroupId);
@ -993,27 +1106,29 @@ function printGroupCandidates() {
function printGroupMembers() { function printGroupMembers() {
const storageKey = 'GroupMembers_PerPage'; const storageKey = 'GroupMembers_PerPage';
$("#rm_group_members_pagination").pagination({ $(".rm_group_members_pagination").each(function() {
dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }), $(this).pagination({
pageRange: 1, dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
position: 'top', pageRange: 1,
showPageNumbers: false, position: 'top',
prevText: '<', showPageNumbers: false,
nextText: '>', prevText: '<',
formatNavigator: PAGINATION_TEMPLATE, nextText: '>',
showNavigator: true, formatNavigator: PAGINATION_TEMPLATE,
showSizeChanger: true, showNavigator: true,
pageSize: Number(localStorage.getItem(storageKey)) || 5, showSizeChanger: true,
sizeChangerOptions: [5, 10, 25, 50, 100, 200], pageSize: Number(localStorage.getItem(storageKey)) || 5,
afterSizeSelectorChange: function (e) { sizeChangerOptions: [5, 10, 25, 50, 100, 200],
localStorage.setItem(storageKey, e.target.value); afterSizeSelectorChange: function (e) {
}, localStorage.setItem(storageKey, e.target.value);
callback: function (data) { },
$("#rm_group_members").empty(); callback: function (data) {
for (const i of data) { $(".rm_group_members").empty();
$("#rm_group_members").append(getGroupCharacterBlock(i.item)); for (const i of data) {
} $(".rm_group_members").append(getGroupCharacterBlock(i.item));
}, }
},
});
}); });
} }
@ -1083,12 +1198,16 @@ function select_group_chats(groupId, skipAnimation) {
const group = openGroupId && groups.find((x) => x.id == openGroupId); const group = openGroupId && groups.find((x) => x.id == openGroupId);
const groupName = group?.name ?? ""; const groupName = group?.name ?? "";
const replyStrategy = Number(group?.activation_strategy ?? group_activation_strategy.NATURAL); const replyStrategy = Number(group?.activation_strategy ?? group_activation_strategy.NATURAL);
const generationMode = Number(group?.generation_mode ?? group_generation_mode.SWAP);
setMenuType(!!group ? 'group_edit' : 'group_create'); setMenuType(!!group ? 'group_edit' : 'group_create');
$("#group_avatar_preview").empty().append(getGroupAvatar(group)); $("#group_avatar_preview").empty().append(getGroupAvatar(group));
$("#rm_group_restore_avatar").toggle(!!group && isValidImageUrl(group.avatar_url)); $("#rm_group_restore_avatar").toggle(!!group && isValidImageUrl(group.avatar_url));
$("#rm_group_filter").val("").trigger("input"); $("#rm_group_filter").val("").trigger("input");
$(`input[name="rm_group_activation_strategy"][value="${replyStrategy}"]`).prop('checked', true); $("#rm_group_activation_strategy").val(replyStrategy);
$(`#rm_group_activation_strategy option[value="${replyStrategy}"]`).prop('selected', true);
$("#rm_group_generation_mode").val(generationMode);
$(`#rm_group_generation_mode option[value="${generationMode}"]`).prop('selected', true);
$("#rm_group_chat_name").val(groupName); $("#rm_group_chat_name").val(groupName);
if (!skipAnimation) { if (!skipAnimation) {
@ -1108,6 +1227,7 @@ function select_group_chats(groupId, skipAnimation) {
$("#rm_group_submit").hide(); $("#rm_group_submit").hide();
$("#rm_group_delete").show(); $("#rm_group_delete").show();
$("#rm_group_scenario").show(); $("#rm_group_scenario").show();
$('#group-metadata-controls .chat_lorebook_button').removeClass('disabled').prop('disabled', false);
} else { } else {
$("#rm_group_submit").show(); $("#rm_group_submit").show();
if ($("#groupAddMemberListToggle .inline-drawer-content").css('display') !== 'block') { if ($("#groupAddMemberListToggle .inline-drawer-content").css('display') !== 'block') {
@ -1115,6 +1235,7 @@ function select_group_chats(groupId, skipAnimation) {
} }
$("#rm_group_delete").hide(); $("#rm_group_delete").hide();
$("#rm_group_scenario").hide(); $("#rm_group_scenario").hide();
$('#group-metadata-controls .chat_lorebook_button').addClass('disabled').prop('disabled', true);
} }
updateFavButtonState(group?.fav ?? false); updateFavButtonState(group?.fav ?? false);
@ -1307,8 +1428,9 @@ function filterGroupMembers() {
async function createGroup() { async function createGroup() {
let name = $("#rm_group_chat_name").val(); let name = $("#rm_group_chat_name").val();
let allow_self_responses = !!$("#rm_group_allow_self_responses").prop("checked"); let allowSelfResponses = !!$("#rm_group_allow_self_responses").prop("checked");
let activation_strategy = $('input[name="rm_group_activation_strategy"]:checked').val() ?? group_activation_strategy.NATURAL; let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL;
let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP;
const members = newGroupMembers; const members = newGroupMembers;
const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(", "); const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(", ");
@ -1328,8 +1450,9 @@ async function createGroup() {
name: name, name: name,
members: members, members: members,
avatar_url: isValidImageUrl(avatar_url) ? avatar_url : default_avatar, avatar_url: isValidImageUrl(avatar_url) ? avatar_url : default_avatar,
allow_self_responses: allow_self_responses, allow_self_responses: allowSelfResponses,
activation_strategy: activation_strategy, activation_strategy: activationStrategy,
generation_mode: generationMode,
disabled_members: [], disabled_members: [],
chat_metadata: {}, chat_metadata: {},
fav: fav_grp_checked, fav: fav_grp_checked,
@ -1555,11 +1678,17 @@ function doCurMemberListPopout() {
<div id="groupMemberListPopoutClose" class="fa-solid fa-circle-xmark hoverglow"></div> <div id="groupMemberListPopoutClose" class="fa-solid fa-circle-xmark hoverglow"></div>
</div>` </div>`
const newElement = $(template); const newElement = $(template);
newElement.attr('id', 'groupMemberListPopout');
newElement.removeClass('zoomed_avatar')
newElement.empty()
newElement.append(controlBarHtml).append(memberListClone) newElement.attr('id', 'groupMemberListPopout')
.removeClass('zoomed_avatar')
.addClass('draggable')
.empty()
.append(controlBarHtml)
.append(memberListClone)
// Remove pagination from popout
newElement.find('.group_pagination').empty();
$('body').append(newElement); $('body').append(newElement);
loadMovingUIState(); loadMovingUIState();
$("#groupMemberListPopout").fadeIn(250) $("#groupMemberListPopout").fadeIn(250)
@ -1568,6 +1697,8 @@ function doCurMemberListPopout() {
$("#groupMemberListPopout").fadeOut(250, () => { $("#groupMemberListPopout").remove() }) $("#groupMemberListPopout").fadeOut(250, () => { $("#groupMemberListPopout").remove() })
}) })
// Re-add pagination not working in popout
printGroupMembers();
} else { } else {
console.debug('saw existing popout, removing') console.debug('saw existing popout, removing')
$("#groupMemberListPopout").fadeOut(250, () => { $("#groupMemberListPopout").remove() }); $("#groupMemberListPopout").fadeOut(250, () => { $("#groupMemberListPopout").remove() });
@ -1593,7 +1724,8 @@ jQuery(() => {
$("#rm_group_delete").off().on("click", onDeleteGroupClick); $("#rm_group_delete").off().on("click", onDeleteGroupClick);
$("#group_favorite_button").on('click', onFavoriteGroupClick); $("#group_favorite_button").on('click', onFavoriteGroupClick);
$("#rm_group_allow_self_responses").on("input", onGroupSelfResponsesClick); $("#rm_group_allow_self_responses").on("input", onGroupSelfResponsesClick);
$('input[name="rm_group_activation_strategy"]').on("input", onGroupActivationStrategyInput); $("#rm_group_activation_strategy").on("change", onGroupActivationStrategyInput);
$("#rm_group_generation_mode").on("change", onGroupGenerationModeInput);
$("#group_avatar_button").on("input", uploadGroupAvatar); $("#group_avatar_button").on("input", uploadGroupAvatar);
$("#rm_group_restore_avatar").on("click", restoreGroupAvatar); $("#rm_group_restore_avatar").on("click", restoreGroupAvatar);
$(document).on("click", ".group_member .right_menu_button", onGroupActionClick); $(document).on("click", ".group_member .right_menu_button", onGroupActionClick);

View File

@ -19,7 +19,7 @@ export {
loadHordeSettings, loadHordeSettings,
adjustHordeGenerationParams, adjustHordeGenerationParams,
getHordeModels, getHordeModels,
MIN_AMOUNT_GEN, MIN_LENGTH,
} }
let models = []; let models = [];
@ -33,7 +33,7 @@ let horde_settings = {
const MAX_RETRIES = 240; const MAX_RETRIES = 240;
const CHECK_INTERVAL = 5000; const CHECK_INTERVAL = 5000;
const MIN_AMOUNT_GEN = 16; const MIN_LENGTH = 16;
const getRequestArgs = () => ({ const getRequestArgs = () => ({
method: "GET", method: "GET",
headers: { headers: {
@ -73,6 +73,11 @@ async function adjustHordeGenerationParams(max_context_length, max_length) {
for (const model of selectedModels) { for (const model of selectedModels) {
for (const worker of workers) { for (const worker of workers) {
if (model.cluster == worker.cluster && worker.models.includes(model.name)) { if (model.cluster == worker.cluster && worker.models.includes(model.name)) {
// Skip workers that are not trusted if the option is enabled
if (horde_settings.trusted_workers_only && !worker.trusted) {
continue;
}
availableWorkers.push(worker); availableWorkers.push(worker);
} }
} }
@ -92,7 +97,15 @@ async function adjustHordeGenerationParams(max_context_length, max_length) {
return { maxContextLength, maxLength }; return { maxContextLength, maxLength };
} }
async function generateHorde(prompt, params, signal) { function setContextSizePreview() {
if (horde_settings.models.length) {
adjustHordeGenerationParams(max_context, amount_gen);
} else {
$("#adjustedHordeParams").text(`Context: --, Response: --`);
}
}
async function generateHorde(prompt, params, signal, reportProgress) {
validateHordeModel(); validateHordeModel();
delete params.prompt; delete params.prompt;
@ -164,7 +177,7 @@ async function generateHorde(prompt, params, signal) {
} }
if (statusCheckJson.done && Array.isArray(statusCheckJson.generations) && statusCheckJson.generations.length) { if (statusCheckJson.done && Array.isArray(statusCheckJson.generations) && statusCheckJson.generations.length) {
setGenerationProgress(100); reportProgress && setGenerationProgress(100);
const generatedText = statusCheckJson.generations[0].text; const generatedText = statusCheckJson.generations[0].text;
const WorkerName = statusCheckJson.generations[0].worker_name; const WorkerName = statusCheckJson.generations[0].worker_name;
const WorkerModel = statusCheckJson.generations[0].model; const WorkerModel = statusCheckJson.generations[0].model;
@ -174,12 +187,12 @@ async function generateHorde(prompt, params, signal) {
} }
else if (!queue_position_first) { else if (!queue_position_first) {
queue_position_first = statusCheckJson.queue_position; queue_position_first = statusCheckJson.queue_position;
setGenerationProgress(0); reportProgress && setGenerationProgress(0);
} }
else if (statusCheckJson.queue_position >= 0) { else if (statusCheckJson.queue_position >= 0) {
let queue_position = statusCheckJson.queue_position; let queue_position = statusCheckJson.queue_position;
const progress = Math.round(100 - (queue_position / queue_position_first * 100)); const progress = Math.round(100 - (queue_position / queue_position_first * 100));
setGenerationProgress(progress); reportProgress && setGenerationProgress(progress);
} }
await delay(CHECK_INTERVAL); await delay(CHECK_INTERVAL);
@ -213,6 +226,8 @@ async function getHordeModels() {
if (horde_settings.models.length && models.filter(m => horde_settings.models.includes(m.name)).length === 0) { if (horde_settings.models.length && models.filter(m => horde_settings.models.includes(m.name)).length === 0) {
horde_settings.models = []; horde_settings.models = [];
} }
setContextSizePreview();
} }
function loadHordeSettings(settings) { function loadHordeSettings(settings) {
@ -263,26 +278,19 @@ jQuery(function () {
$("#horde_auto_adjust_response_length").on("input", function () { $("#horde_auto_adjust_response_length").on("input", function () {
horde_settings.auto_adjust_response_length = !!$(this).prop("checked"); horde_settings.auto_adjust_response_length = !!$(this).prop("checked");
if (horde_settings.models.length) { setContextSizePreview();
adjustHordeGenerationParams(max_context, amount_gen)
} else {
$("#adjustedHordeParams").text(`Context: --, Response: --`)
}
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$("#horde_auto_adjust_context_length").on("input", function () { $("#horde_auto_adjust_context_length").on("input", function () {
horde_settings.auto_adjust_context_length = !!$(this).prop("checked"); horde_settings.auto_adjust_context_length = !!$(this).prop("checked");
if (horde_settings.models.length) { setContextSizePreview();
adjustHordeGenerationParams(max_context, amount_gen);
} else {
$("#adjustedHordeParams").text(`Context: --, Response: --`)
}
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$("#horde_trusted_workers_only").on("input", function () { $("#horde_trusted_workers_only").on("input", function () {
horde_settings.trusted_workers_only = !!$(this).prop("checked"); horde_settings.trusted_workers_only = !!$(this).prop("checked");
setContextSizePreview();
saveSettingsDebounced(); saveSettingsDebounced();
}) })
@ -313,3 +321,4 @@ jQuery(function () {
}); });
} }
}) })

View File

@ -335,7 +335,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
text += (includeNames ? promptBias : (separator + promptBias)); text += (includeNames ? promptBias : (separator + promptBias));
} }
return text.trimEnd() + (includeNames ? '' : separator); return (power_user.instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);
} }
/** /**

View File

@ -72,7 +72,7 @@ export function loadKoboldSettings(preset) {
const formattedValue = slider.format(value); const formattedValue = slider.format(value);
slider.setValue(value); slider.setValue(value);
$(slider.sliderId).val(value); $(slider.sliderId).val(value);
$(slider.counterId).text(formattedValue); $(slider.counterId).val(formattedValue);
} }
// TODO: refactor checkboxes (if adding any more) // TODO: refactor checkboxes (if adding any more)
@ -90,15 +90,26 @@ export function loadKoboldSettings(preset) {
} }
} }
export function getKoboldGenerationData(finalPrompt, this_settings, this_amount_gen, this_max_context, isImpersonate, type) { /**
const sampler_order = kai_settings.sampler_order || this_settings.sampler_order; * Gets the Kobold generation data.
* @param {string} finalPrompt Final text prompt.
* @param {object} settings Settings preset object.
* @param {number} maxLength Maximum length.
* @param {number} maxContextLength Maximum context length.
* @param {boolean} isHorde True if the generation is for a horde, false otherwise.
* @param {string} type Generation type.
* @returns {object} Kobold generation data.
*/
export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxContextLength, isHorde, type) {
const isImpersonate = type === 'impersonate';
const sampler_order = kai_settings.sampler_order || settings.sampler_order;
let generate_data = { let generate_data = {
prompt: finalPrompt, prompt: finalPrompt,
gui_settings: false, gui_settings: false,
sampler_order: sampler_order, sampler_order: sampler_order,
max_context_length: Number(this_max_context), max_context_length: Number(maxContextLength),
max_length: this_amount_gen, max_length: maxLength,
rep_pen: Number(kai_settings.rep_pen), rep_pen: Number(kai_settings.rep_pen),
rep_pen_range: Number(kai_settings.rep_pen_range), rep_pen_range: Number(kai_settings.rep_pen_range),
rep_pen_slope: kai_settings.rep_pen_slope, rep_pen_slope: kai_settings.rep_pen_slope,
@ -117,7 +128,7 @@ export function getKoboldGenerationData(finalPrompt, this_settings, this_amount_
s7: sampler_order[6], s7: sampler_order[6],
use_world_info: false, use_world_info: false,
singleline: kai_settings.single_line, singleline: kai_settings.single_line,
stop_sequence: kai_flags.can_use_stop_sequence ? getStoppingStrings(isImpersonate) : undefined, stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate) : undefined,
streaming: kai_settings.streaming_kobold && kai_flags.can_use_streaming && type !== 'quiet', streaming: kai_settings.streaming_kobold && kai_flags.can_use_streaming && type !== 'quiet',
can_abort: kai_flags.can_use_streaming, can_abort: kai_flags.can_use_streaming,
mirostat: kai_flags.can_use_mirostat ? kai_settings.mirostat : undefined, mirostat: kai_flags.can_use_mirostat ? kai_settings.mirostat : undefined,
@ -364,7 +375,7 @@ jQuery(function () {
const value = $(this).val(); const value = $(this).val();
const formattedValue = slider.format(value); const formattedValue = slider.format(value);
slider.setValue(value); slider.setValue(value);
$(slider.counterId).text(formattedValue); $(slider.counterId).val(formattedValue);
saveSettingsDebounced(); saveSettingsDebounced();
}); });
}); });

View File

@ -5,7 +5,7 @@ import {
saveSettingsDebounced, saveSettingsDebounced,
setGenerationParamsFromPreset setGenerationParamsFromPreset
} from "../script.js"; } from "../script.js";
import { getCfgPrompt } from "./extensions/cfg/util.js"; import { getCfgPrompt } from "./cfg-scale.js";
import { MAX_CONTEXT_DEFAULT } from "./power-user.js"; import { MAX_CONTEXT_DEFAULT } from "./power-user.js";
import { getTextTokens, tokenizers } from "./tokenizers.js"; import { getTextTokens, tokenizers } from "./tokenizers.js";
import { import {
@ -176,36 +176,36 @@ export function loadNovelSettings(settings) {
function loadNovelSettingsUi(ui_settings) { function loadNovelSettingsUi(ui_settings) {
$("#temp_novel").val(ui_settings.temperature); $("#temp_novel").val(ui_settings.temperature);
$("#temp_counter_novel").text(Number(ui_settings.temperature).toFixed(2)); $("#temp_counter_novel").val(Number(ui_settings.temperature).toFixed(2));
$("#rep_pen_novel").val(ui_settings.repetition_penalty); $("#rep_pen_novel").val(ui_settings.repetition_penalty);
$("#rep_pen_counter_novel").text(Number(ui_settings.repetition_penalty).toFixed(2)); $("#rep_pen_counter_novel").val(Number(ui_settings.repetition_penalty).toFixed(2));
$("#rep_pen_size_novel").val(ui_settings.repetition_penalty_range); $("#rep_pen_size_novel").val(ui_settings.repetition_penalty_range);
$("#rep_pen_size_counter_novel").text(Number(ui_settings.repetition_penalty_range).toFixed(0)); $("#rep_pen_size_counter_novel").val(Number(ui_settings.repetition_penalty_range).toFixed(0));
$("#rep_pen_slope_novel").val(ui_settings.repetition_penalty_slope); $("#rep_pen_slope_novel").val(ui_settings.repetition_penalty_slope);
$("#rep_pen_slope_counter_novel").text(Number(`${ui_settings.repetition_penalty_slope}`).toFixed(2)); $("#rep_pen_slope_counter_novel").val(Number(`${ui_settings.repetition_penalty_slope}`).toFixed(2));
$("#rep_pen_freq_novel").val(ui_settings.repetition_penalty_frequency); $("#rep_pen_freq_novel").val(ui_settings.repetition_penalty_frequency);
$("#rep_pen_freq_counter_novel").text(Number(ui_settings.repetition_penalty_frequency).toFixed(5)); $("#rep_pen_freq_counter_novel").val(Number(ui_settings.repetition_penalty_frequency).toFixed(2));
$("#rep_pen_presence_novel").val(ui_settings.repetition_penalty_presence); $("#rep_pen_presence_novel").val(ui_settings.repetition_penalty_presence);
$("#rep_pen_presence_counter_novel").text(Number(ui_settings.repetition_penalty_presence).toFixed(3)); $("#rep_pen_presence_counter_novel").val(Number(ui_settings.repetition_penalty_presence).toFixed(2));
$("#tail_free_sampling_novel").val(ui_settings.tail_free_sampling); $("#tail_free_sampling_novel").val(ui_settings.tail_free_sampling);
$("#tail_free_sampling_counter_novel").text(Number(ui_settings.tail_free_sampling).toFixed(3)); $("#tail_free_sampling_counter_novel").val(Number(ui_settings.tail_free_sampling).toFixed(3));
$("#top_k_novel").val(ui_settings.top_k); $("#top_k_novel").val(ui_settings.top_k);
$("#top_k_counter_novel").text(Number(ui_settings.top_k).toFixed(0)); $("#top_k_counter_novel").val(Number(ui_settings.top_k).toFixed(0));
$("#top_p_novel").val(ui_settings.top_p); $("#top_p_novel").val(ui_settings.top_p);
$("#top_p_counter_novel").text(Number(ui_settings.top_p).toFixed(2)); $("#top_p_counter_novel").val(Number(ui_settings.top_p).toFixed(3));
$("#top_a_novel").val(ui_settings.top_a); $("#top_a_novel").val(ui_settings.top_a);
$("#top_a_counter_novel").text(Number(ui_settings.top_a).toFixed(2)); $("#top_a_counter_novel").val(Number(ui_settings.top_a).toFixed(2));
$("#typical_p_novel").val(ui_settings.typical_p); $("#typical_p_novel").val(ui_settings.typical_p);
$("#typical_p_counter_novel").text(Number(ui_settings.typical_p).toFixed(3)); $("#typical_p_counter_novel").val(Number(ui_settings.typical_p).toFixed(2));
$("#cfg_scale_novel").val(ui_settings.cfg_scale); $("#cfg_scale_novel").val(ui_settings.cfg_scale);
$("#cfg_scale_counter_novel").text(Number(ui_settings.cfg_scale).toFixed(2)); $("#cfg_scale_counter_novel").val(Number(ui_settings.cfg_scale).toFixed(2));
$("#phrase_rep_pen_novel").val(ui_settings.phrase_rep_pen || "off"); $("#phrase_rep_pen_novel").val(ui_settings.phrase_rep_pen || "off");
$("#mirostat_lr_novel").val(ui_settings.mirostat_lr); $("#mirostat_lr_novel").val(ui_settings.mirostat_lr);
$("#mirostat_lr_counter_novel").text(Number(ui_settings.mirostat_lr).toFixed(2)); $("#mirostat_lr_counter_novel").val(Number(ui_settings.mirostat_lr).toFixed(2));
$("#mirostat_tau_novel").val(ui_settings.mirostat_tau); $("#mirostat_tau_novel").val(ui_settings.mirostat_tau);
$("#mirostat_tau_counter_novel").text(Number(ui_settings.mirostat_tau).toFixed(2)); $("#mirostat_tau_counter_novel").val(Number(ui_settings.mirostat_tau).toFixed(2));
$("#min_length_novel").val(ui_settings.min_length); $("#min_length_novel").val(ui_settings.min_length);
$("#min_length_counter_novel").text(Number(ui_settings.min_length).toFixed(0)); $("#min_length_counter_novel").val(Number(ui_settings.min_length).toFixed(0));
$('#nai_preamble_textarea').val(ui_settings.preamble); $('#nai_preamble_textarea').val(ui_settings.preamble);
$('#nai_prefix').val(ui_settings.prefix || "vanilla"); $('#nai_prefix').val(ui_settings.prefix || "vanilla");
$('#nai_cfg_uc').val(ui_settings.cfg_uc || ""); $('#nai_cfg_uc').val(ui_settings.cfg_uc || "");
@ -244,14 +244,14 @@ const sliders = [
{ {
sliderId: "#rep_pen_freq_novel", sliderId: "#rep_pen_freq_novel",
counterId: "#rep_pen_freq_counter_novel", counterId: "#rep_pen_freq_counter_novel",
format: (val) => `${val}`, format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.repetition_penalty_frequency = Number(val).toFixed(5); }, setValue: (val) => { nai_settings.repetition_penalty_frequency = Number(val).toFixed(2); },
}, },
{ {
sliderId: "#rep_pen_presence_novel", sliderId: "#rep_pen_presence_novel",
counterId: "#rep_pen_presence_counter_novel", counterId: "#rep_pen_presence_counter_novel",
format: (val) => `${val}`, format: (val) => `${val}`,
setValue: (val) => { nai_settings.repetition_penalty_presence = Number(val).toFixed(3); }, setValue: (val) => { nai_settings.repetition_penalty_presence = Number(val).toFixed(2); },
}, },
{ {
sliderId: "#tail_free_sampling_novel", sliderId: "#tail_free_sampling_novel",
@ -268,8 +268,8 @@ const sliders = [
{ {
sliderId: "#top_p_novel", sliderId: "#top_p_novel",
counterId: "#top_p_counter_novel", counterId: "#top_p_counter_novel",
format: (val) => Number(val).toFixed(2), format: (val) => Number(val).toFixed(3),
setValue: (val) => { nai_settings.top_p = Number(val).toFixed(2); }, setValue: (val) => { nai_settings.top_p = Number(val).toFixed(3); },
}, },
{ {
sliderId: "#top_a_novel", sliderId: "#top_a_novel",
@ -280,8 +280,8 @@ const sliders = [
{ {
sliderId: "#typical_p_novel", sliderId: "#typical_p_novel",
counterId: "#typical_p_counter_novel", counterId: "#typical_p_counter_novel",
format: (val) => Number(val).toFixed(3), format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(3); }, setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(2); },
}, },
{ {
sliderId: "#mirostat_tau_novel", sliderId: "#mirostat_tau_novel",
@ -740,7 +740,7 @@ jQuery(function () {
const value = $(this).val(); const value = $(this).val();
const formattedValue = slider.format(value); const formattedValue = slider.format(value);
slider.setValue(value); slider.setValue(value);
$(slider.counterId).text(formattedValue); $(slider.counterId).val(formattedValue);
saveSettingsDebounced(); saveSettingsDebounced();
}); });
}); });

View File

@ -31,7 +31,8 @@ import { groups, selected_group } from "./group-chats.js";
import { import {
promptManagerDefaultPromptOrders, promptManagerDefaultPromptOrders,
chatCompletionDefaultPrompts, Prompt, chatCompletionDefaultPrompts, Prompt,
PromptManagerModule as PromptManager PromptManagerModule as PromptManager,
INJECTION_POSITION,
} from "./PromptManager.js"; } from "./PromptManager.js";
import { import {
@ -64,7 +65,6 @@ export {
setOpenAIMessages, setOpenAIMessages,
setOpenAIMessageExamples, setOpenAIMessageExamples,
setupChatCompletionPromptManager, setupChatCompletionPromptManager,
generateOpenAIPromptCache,
prepareOpenAIMessages, prepareOpenAIMessages,
sendOpenAIRequest, sendOpenAIRequest,
setOpenAIOnlineStatus, setOpenAIOnlineStatus,
@ -217,6 +217,7 @@ const default_settings = {
use_ai21_tokenizer: false, use_ai21_tokenizer: false,
exclude_assistant: false, exclude_assistant: false,
use_alt_scale: false, use_alt_scale: false,
squash_system_messages: false,
}; };
const oai_settings = { const oai_settings = {
@ -261,6 +262,7 @@ const oai_settings = {
use_ai21_tokenizer: false, use_ai21_tokenizer: false,
exclude_assistant: false, exclude_assistant: false,
use_alt_scale: false, use_alt_scale: false,
squash_system_messages: false,
}; };
let openai_setting_names; let openai_setting_names;
@ -321,15 +323,6 @@ function setOpenAIMessages(chat) {
openai_msgs[i] = { "role": role, "content": content, name: name }; openai_msgs[i] = { "role": role, "content": content, name: name };
j++; j++;
} }
// Add chat injections, 100 = maximum depth of injection. (Why would you ever need more?)
for (let i = MAX_INJECTION_DEPTH; i >= 0; i--) {
const anchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, i);
if (anchor && anchor.length) {
openai_msgs.splice(i, 0, { "role": 'system', 'content': anchor.trim() });
}
}
} }
function setOpenAIMessageExamples(mesExamplesArray) { function setOpenAIMessageExamples(mesExamplesArray) {
@ -395,15 +388,6 @@ function setupChatCompletionPromptManager(openAiSettings) {
return promptManager; return promptManager;
} }
function generateOpenAIPromptCache() {
openai_msgs = openai_msgs.reverse();
openai_msgs.forEach(function (msg, i, arr) {
let item = msg["content"];
msg["content"] = item;
openai_msgs[i] = msg;
});
}
function parseExampleIntoIndividual(messageExampleString) { function parseExampleIntoIndividual(messageExampleString) {
let result = []; // array of msgs let result = []; // array of msgs
let tmp = messageExampleString.split("\n"); let tmp = messageExampleString.split("\n");
@ -468,6 +452,45 @@ function formatWorldInfo(value) {
return stringFormat(oai_settings.wi_format, value); return stringFormat(oai_settings.wi_format, value);
} }
/**
* This function populates the injections in the conversation.
*
* @param {Prompt[]} prompts - Array containing injection prompts.
*/
function populationInjectionPrompts(prompts) {
let totalInsertedMessages = 0;
for (let i = 0; i <= MAX_INJECTION_DEPTH; i++) {
// Get prompts for current depth
const depthPrompts = prompts.filter(prompt => prompt.injection_depth === i && prompt.content);
// Order of priority (most important go lower)
const roles = ['system', 'user', 'assistant'];
const roleMessages = [];
for (const role of roles) {
// Get prompts for current role
const rolePrompts = depthPrompts.filter(prompt => prompt.role === role).map(x => x.content).join('\n');
// Get extension prompt (only for system role)
const extensionPrompt = role === 'system' ? getExtensionPrompt(extension_prompt_types.IN_CHAT, i) : '';
const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join('\n');
if (jointPrompt && jointPrompt.length) {
roleMessages.push({ "role": role, 'content': jointPrompt });
}
}
if (roleMessages.length) {
const injectIdx = i + totalInsertedMessages;
openai_msgs.splice(injectIdx, 0, ...roleMessages);
totalInsertedMessages += roleMessages.length;
}
}
openai_msgs = openai_msgs.reverse();
}
/** /**
* Populates the chat history of the conversation. * Populates the chat history of the conversation.
* *
@ -477,7 +500,6 @@ function formatWorldInfo(value) {
* @param cyclePrompt * @param cyclePrompt
*/ */
function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = null) { function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = null) {
// Chat History
chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory')); chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory'));
let names = (selected_group && groups.find(x => x.id === selected_group)?.members.map(member => characters.find(c => c.avatar === member)?.name).filter(Boolean).join(', ')) || ''; let names = (selected_group && groups.find(x => x.id === selected_group)?.members.map(member => characters.find(c => c.avatar === member)?.name).filter(Boolean).join(', ')) || '';
@ -640,20 +662,26 @@ function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, ty
// Add quiet prompt to control prompts // Add quiet prompt to control prompts
// This should always be last, even in control prompts. Add all further control prompts BEFORE this prompt // This should always be last, even in control prompts. Add all further control prompts BEFORE this prompt
const quietPromptMessage = Message.fromPrompt(prompts.get('quietPrompt')) ?? null; const quietPromptMessage = Message.fromPrompt(prompts.get('quietPrompt')) ?? null;
if (quietPromptMessage) controlPrompts.add(quietPromptMessage); if (quietPromptMessage && quietPromptMessage.content) controlPrompts.add(quietPromptMessage);
chatCompletion.reserveBudget(controlPrompts); chatCompletion.reserveBudget(controlPrompts);
// Add ordered system and user prompts // Add ordered system and user prompts
const systemPrompts = ['nsfw', 'jailbreak']; const systemPrompts = ['nsfw', 'jailbreak'];
const userPrompts = prompts.collection const userRelativePrompts = prompts.collection
.filter((prompt) => false === prompt.system_prompt) .filter((prompt) => false === prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE)
.reduce((acc, prompt) => { .reduce((acc, prompt) => {
acc.push(prompt.identifier) acc.push(prompt.identifier)
return acc; return acc;
}, []); }, []);
const userAbsolutePrompts = prompts.collection
.filter((prompt) => false === prompt.system_prompt && prompt.injection_position === INJECTION_POSITION.ABSOLUTE)
.reduce((acc, prompt) => {
acc.push(prompt)
return acc;
}, []);
[...systemPrompts, ...userPrompts].forEach(identifier => addToChatCompletion(identifier)); [...systemPrompts, ...userRelativePrompts].forEach(identifier => addToChatCompletion(identifier));
// Add enhance definition instruction // Add enhance definition instruction
if (prompts.has('enhanceDefinitions')) addToChatCompletion('enhanceDefinitions'); if (prompts.has('enhanceDefinitions')) addToChatCompletion('enhanceDefinitions');
@ -697,6 +725,9 @@ function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, ty
} }
} }
// Add in-chat injections
populationInjectionPrompts(userAbsolutePrompts);
// Decide whether dialogue examples should always be added // Decide whether dialogue examples should always be added
if (power_user.pin_examples) { if (power_user.pin_examples) {
populateDialogueExamples(prompts, chatCompletion); populateDialogueExamples(prompts, chatCompletion);
@ -908,6 +939,10 @@ function prepareOpenAIMessages({
// Pass chat completion to prompt manager for inspection // Pass chat completion to prompt manager for inspection
promptManager.setChatCompletion(chatCompletion); promptManager.setChatCompletion(chatCompletion);
if (oai_settings.squash_system_messages) {
chatCompletion.squashSystemMessages();
}
// All information is up-to-date, render. // All information is up-to-date, render.
if (false === dryRun) promptManager.render(false); if (false === dryRun) promptManager.render(false);
} }
@ -1617,6 +1652,21 @@ class MessageCollection {
getTokens() { getTokens() {
return this.collection.reduce((tokens, message) => tokens + message.getTokens(), 0); return this.collection.reduce((tokens, message) => tokens + message.getTokens(), 0);
} }
/**
* Combines message collections into a single collection.
* @returns {Message[]} The collection of messages flattened into a single array.
*/
flatten() {
return this.collection.reduce((acc, message) => {
if (message instanceof MessageCollection) {
acc.push(...message.flatten());
} else {
acc.push(message);
}
return acc;
}, []);
}
} }
/** /**
@ -1631,6 +1681,36 @@ class MessageCollection {
*/ */
class ChatCompletion { class ChatCompletion {
/**
* Combines consecutive system messages into one if they have no name attached.
*/
squashSystemMessages() {
const excludeList = ['newMainChat', 'newChat', 'groupNudge'];
this.messages.collection = this.messages.flatten();
let lastMessage = null;
let squashedMessages = [];
for (let message of this.messages.collection) {
if (!excludeList.includes(message.identifier) && message.role === 'system' && !message.name) {
if (lastMessage && lastMessage.role === 'system') {
lastMessage.content += '\n' + message.content;
lastMessage.tokens = tokenHandler.count({ role: lastMessage.role, content: lastMessage.content });
}
else {
squashedMessages.push(message);
lastMessage = message;
}
}
else {
squashedMessages.push(message);
lastMessage = message;
}
}
this.messages.collection = squashedMessages;
}
/** /**
* Initializes a new instance of ChatCompletion. * Initializes a new instance of ChatCompletion.
* Sets up the initial token budget and a new message collection. * Sets up the initial token budget and a new message collection.
@ -1714,7 +1794,7 @@ class ChatCompletion {
* *
* @param {Message} message - The message to insert. * @param {Message} message - The message to insert.
* @param {string} identifier - The identifier of the collection where to insert the message. * @param {string} identifier - The identifier of the collection where to insert the message.
* @param {string} position - The position at which to insert the message ('start' or 'end'). * @param {string|number} position - The position at which to insert the message ('start' or 'end').
*/ */
insert(message, identifier, position = 'end') { insert(message, identifier, position = 'end') {
this.validateMessage(message); this.validateMessage(message);
@ -1723,7 +1803,8 @@ class ChatCompletion {
const index = this.findMessageIndex(identifier); const index = this.findMessageIndex(identifier);
if (message.content) { if (message.content) {
if ('start' === position) this.messages.collection[index].collection.unshift(message); if ('start' === position) this.messages.collection[index].collection.unshift(message);
else if ('end' === position) this.messages.collection[index].collection.push(message); else if ('end' === position) this.messages.collection[index].collection.push(message)
else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);
this.decreaseTokenBudgetBy(message.getTokens()); this.decreaseTokenBudgetBy(message.getTokens());
@ -1731,7 +1812,6 @@ class ChatCompletion {
} }
} }
/** /**
* Remove the last item of the collection * Remove the last item of the collection
* *
@ -1790,7 +1870,11 @@ class ChatCompletion {
for (let item of this.messages.collection) { for (let item of this.messages.collection) {
if (item instanceof MessageCollection) { if (item instanceof MessageCollection) {
chat.push(...item.getChat()); chat.push(...item.getChat());
} else if (item instanceof Message && item.content) {
const message = { role: item.role, content: item.content, ...(item.name ? { name: item.name } : {}) };
chat.push(message);
} else { } else {
this.log(`Item ${item} has an unknown type. Adding as-is`);
chat.push(item); chat.push(item);
} }
} }
@ -1964,6 +2048,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.new_group_chat_prompt = settings.new_group_chat_prompt ?? default_settings.new_group_chat_prompt; oai_settings.new_group_chat_prompt = settings.new_group_chat_prompt ?? default_settings.new_group_chat_prompt;
oai_settings.new_example_chat_prompt = settings.new_example_chat_prompt ?? default_settings.new_example_chat_prompt; oai_settings.new_example_chat_prompt = settings.new_example_chat_prompt ?? default_settings.new_example_chat_prompt;
oai_settings.continue_nudge_prompt = settings.continue_nudge_prompt ?? default_settings.continue_nudge_prompt; oai_settings.continue_nudge_prompt = settings.continue_nudge_prompt ?? default_settings.continue_nudge_prompt;
oai_settings.squash_system_messages = settings.squash_system_messages ?? default_settings.squash_system_messages;
if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes; if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes;
if (settings.names_in_completion !== undefined) oai_settings.names_in_completion = !!settings.names_in_completion; if (settings.names_in_completion !== undefined) oai_settings.names_in_completion = !!settings.names_in_completion;
@ -1985,7 +2070,7 @@ function loadOpenAISettings(data, settings) {
$('#model_ai21_select').val(oai_settings.ai21_model); $('#model_ai21_select').val(oai_settings.ai21_model);
$(`#model_ai21_select option[value="${oai_settings.ai21_model}"`).attr('selected', true); $(`#model_ai21_select option[value="${oai_settings.ai21_model}"`).attr('selected', true);
$('#openai_max_context').val(oai_settings.openai_max_context); $('#openai_max_context').val(oai_settings.openai_max_context);
$('#openai_max_context_counter').text(`${oai_settings.openai_max_context}`); $('#openai_max_context_counter').val(`${oai_settings.openai_max_context}`);
$('#model_openrouter_select').val(oai_settings.openrouter_model); $('#model_openrouter_select').val(oai_settings.openrouter_model);
$('#openai_max_tokens').val(oai_settings.openai_max_tokens); $('#openai_max_tokens').val(oai_settings.openai_max_tokens);
@ -2000,6 +2085,7 @@ function loadOpenAISettings(data, settings) {
$('#exclude_assistant').prop('checked', oai_settings.exclude_assistant); $('#exclude_assistant').prop('checked', oai_settings.exclude_assistant);
$('#scale-alt').prop('checked', oai_settings.use_alt_scale); $('#scale-alt').prop('checked', oai_settings.use_alt_scale);
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback); $('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
$('#squash_system_messages').prop('checked', oai_settings.squash_system_messages);
if (settings.impersonation_prompt !== undefined) oai_settings.impersonation_prompt = settings.impersonation_prompt; if (settings.impersonation_prompt !== undefined) oai_settings.impersonation_prompt = settings.impersonation_prompt;
$('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt); $('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt);
@ -2013,22 +2099,22 @@ function loadOpenAISettings(data, settings) {
$('#send_if_empty_textarea').val(oai_settings.send_if_empty); $('#send_if_empty_textarea').val(oai_settings.send_if_empty);
$('#temp_openai').val(oai_settings.temp_openai); $('#temp_openai').val(oai_settings.temp_openai);
$('#temp_counter_openai').text(Number(oai_settings.temp_openai).toFixed(2)); $('#temp_counter_openai').val(Number(oai_settings.temp_openai).toFixed(2));
$('#freq_pen_openai').val(oai_settings.freq_pen_openai); $('#freq_pen_openai').val(oai_settings.freq_pen_openai);
$('#freq_pen_counter_openai').text(Number(oai_settings.freq_pen_openai).toFixed(2)); $('#freq_pen_counter_openai').val(Number(oai_settings.freq_pen_openai).toFixed(2));
$('#pres_pen_openai').val(oai_settings.pres_pen_openai); $('#pres_pen_openai').val(oai_settings.pres_pen_openai);
$('#pres_pen_counter_openai').text(Number(oai_settings.pres_pen_openai).toFixed(2)); $('#pres_pen_counter_openai').val(Number(oai_settings.pres_pen_openai).toFixed(2));
$('#count_pen').val(oai_settings.count_pen); $('#count_pen').val(oai_settings.count_pen);
$('#count_pen_counter').text(Number(oai_settings.count_pen).toFixed(2)); $('#count_pen_counter').val(Number(oai_settings.count_pen).toFixed(2));
$('#top_p_openai').val(oai_settings.top_p_openai); $('#top_p_openai').val(oai_settings.top_p_openai);
$('#top_p_counter_openai').text(Number(oai_settings.top_p_openai).toFixed(2)); $('#top_p_counter_openai').val(Number(oai_settings.top_p_openai).toFixed(2));
$('#top_k_openai').val(oai_settings.top_k_openai); $('#top_k_openai').val(oai_settings.top_k_openai);
$('#top_k_counter_openai').text(Number(oai_settings.top_k_openai).toFixed(0)); $('#top_k_counter_openai').val(Number(oai_settings.top_k_openai).toFixed(0));
if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy; if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
$('#openai_reverse_proxy').val(oai_settings.reverse_proxy); $('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
@ -2199,6 +2285,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
use_ai21_tokenizer: settings.use_ai21_tokenizer, use_ai21_tokenizer: settings.use_ai21_tokenizer,
exclude_assistant: settings.exclude_assistant, exclude_assistant: settings.exclude_assistant,
use_alt_scale: settings.use_alt_scale, use_alt_scale: settings.use_alt_scale,
squash_system_messages: settings.squash_system_messages,
}; };
const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, { const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, {
@ -2543,14 +2630,14 @@ function onSettingsPresetChange() {
stream_openai: ['#stream_toggle', 'stream_openai', true], stream_openai: ['#stream_toggle', 'stream_openai', true],
prompts: ['', 'prompts', false], prompts: ['', 'prompts', false],
prompt_order: ['', 'prompt_order', false], prompt_order: ['', 'prompt_order', false],
use_openrouter: ['#use_openrouter', 'use_openrouter', true],
api_url_scale: ['#api_url_scale', 'api_url_scale', false], api_url_scale: ['#api_url_scale', 'api_url_scale', false],
show_external_models: ['#openai_show_external_models', 'show_external_models', true], show_external_models: ['#openai_show_external_models', 'show_external_models', true],
proxy_password: ['#openai_proxy_password', 'proxy_password', false], proxy_password: ['#openai_proxy_password', 'proxy_password', false],
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false], assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', false], use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
exclude_assistant: ['#exclude_assistant', 'exclude_assistant', false], exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true],
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', false], use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
}; };
const presetName = $('#settings_perset_openai').find(":selected").text(); const presetName = $('#settings_perset_openai').find(":selected").text();
@ -3032,43 +3119,43 @@ $(document).ready(async function () {
$(document).on('input', '#temp_openai', function () { $(document).on('input', '#temp_openai', function () {
oai_settings.temp_openai = Number($(this).val()); oai_settings.temp_openai = Number($(this).val());
$('#temp_counter_openai').text(Number($(this).val()).toFixed(2)); $('#temp_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$(document).on('input', '#freq_pen_openai', function () { $(document).on('input', '#freq_pen_openai', function () {
oai_settings.freq_pen_openai = Number($(this).val()); oai_settings.freq_pen_openai = Number($(this).val());
$('#freq_pen_counter_openai').text(Number($(this).val()).toFixed(2)); $('#freq_pen_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$(document).on('input', '#pres_pen_openai', function () { $(document).on('input', '#pres_pen_openai', function () {
oai_settings.pres_pen_openai = Number($(this).val()); oai_settings.pres_pen_openai = Number($(this).val());
$('#pres_pen_counter_openai').text(Number($(this).val()).toFixed(2)); $('#pres_pen_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$(document).on('input', '#count_pen', function () { $(document).on('input', '#count_pen', function () {
oai_settings.count_pen = Number($(this).val()); oai_settings.count_pen = Number($(this).val());
$('#count_pen_counter').text(Number($(this).val()).toFixed(2)); $('#count_pen_counter').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$(document).on('input', '#top_p_openai', function () { $(document).on('input', '#top_p_openai', function () {
oai_settings.top_p_openai = Number($(this).val()); oai_settings.top_p_openai = Number($(this).val());
$('#top_p_counter_openai').text(Number($(this).val()).toFixed(2)); $('#top_p_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$(document).on('input', '#top_k_openai', function () { $(document).on('input', '#top_k_openai', function () {
oai_settings.top_k_openai = Number($(this).val()); oai_settings.top_k_openai = Number($(this).val());
$('#top_k_counter_openai').text(Number($(this).val()).toFixed(0)); $('#top_k_counter_openai').val(Number($(this).val()).toFixed(0));
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$(document).on('input', '#openai_max_context', function () { $(document).on('input', '#openai_max_context', function () {
oai_settings.openai_max_context = Number($(this).val()); oai_settings.openai_max_context = Number($(this).val());
$('#openai_max_context_counter').text(`${$(this).val()}`); $('#openai_max_context_counter').val(`${$(this).val()}`);
calculateOpenRouterCost(); calculateOpenRouterCost();
saveSettingsDebounced(); saveSettingsDebounced();
}); });
@ -3257,6 +3344,11 @@ $(document).ready(async function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#squash_system_messages').on('input', function () {
oai_settings.squash_system_messages = !!$(this).prop('checked');
saveSettingsDebounced();
});
$(document).on('input', '#openai_settings .autoSetHeight', function () { $(document).on('input', '#openai_settings .autoSetHeight', function () {
resetScrollHeight($(this)); resetScrollHeight($(this));
}); });

View File

@ -181,7 +181,7 @@ async function bindUserNameToPersona() {
export function selectCurrentPersona() { export function selectCurrentPersona() {
const personaName = power_user.personas[user_avatar]; const personaName = power_user.personas[user_avatar];
if (personaName && name1 !== personaName) { if (personaName) {
const lockedPersona = chat_metadata['persona']; const lockedPersona = chat_metadata['persona'];
if (lockedPersona && lockedPersona !== user_avatar && power_user.persona_show_notifications) { if (lockedPersona && lockedPersona !== user_avatar && power_user.persona_show_notifications) {
toastr.info( toastr.info(
@ -191,7 +191,10 @@ export function selectCurrentPersona() {
); );
} }
setUserName(personaName); if (personaName !== name1) {
console.log(`Auto-updating user name to ${personaName}`);
setUserName(personaName);
}
const descriptor = power_user.persona_descriptions[user_avatar]; const descriptor = power_user.persona_descriptions[user_avatar];

View File

@ -41,10 +41,13 @@ export {
fixMarkdown, fixMarkdown,
power_user, power_user,
send_on_enter_options, send_on_enter_options,
getContextSettings,
}; };
export const MAX_CONTEXT_DEFAULT = 4096; export const MAX_CONTEXT_DEFAULT = 8192;
const MAX_CONTEXT_UNLOCKED = 65536; const MAX_CONTEXT_UNLOCKED = 65536;
const unlockedMaxContextStep = 4096
const unlockedMaxContestMin = 8192
const defaultStoryString = "{{#if system}}{{system}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}"; const defaultStoryString = "{{#if system}}{{system}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}";
const defaultExampleSeparator = '***'; const defaultExampleSeparator = '***';
@ -131,7 +134,6 @@ let power_user = {
custom_css: '', custom_css: '',
waifuMode: false, waifuMode: false,
movingUI: false, movingUI: false,
movingUIState: {}, movingUIState: {},
@ -139,7 +141,7 @@ let power_user = {
noShadows: false, noShadows: false,
theme: 'Default (Dark) 1.7.1', theme: 'Default (Dark) 1.7.1',
gestures: true,
auto_swipe: false, auto_swipe: false,
auto_swipe_minimum_length: 0, auto_swipe_minimum_length: 0,
auto_swipe_blacklist: [], auto_swipe_blacklist: [],
@ -165,6 +167,7 @@ let power_user = {
continue_on_send: false, continue_on_send: false,
trim_spaces: true, trim_spaces: true,
relaxed_api_urls: false, relaxed_api_urls: false,
world_import_dialog: true,
disable_group_trimming: false, disable_group_trimming: false,
default_instruct: '', default_instruct: '',
@ -248,6 +251,20 @@ const storage_keys = {
expand_message_actions: 'ExpandMessageActions', expand_message_actions: 'ExpandMessageActions',
}; };
const contextControls = [
// Power user context scoped settings
{ id: "context_story_string", property: "story_string", isCheckbox: false, isGlobalSetting: false },
{ id: "context_example_separator", property: "example_separator", isCheckbox: false, isGlobalSetting: false },
{ id: "context_chat_start", property: "chat_start", isCheckbox: false, isGlobalSetting: false },
// Existing power user settings
{ id: "always-force-name2-checkbox", property: "always_force_name2", isCheckbox: true, isGlobalSetting: true },
{ id: "trim_sentences_checkbox", property: "trim_sentences", isCheckbox: true, isGlobalSetting: true },
{ id: "include_newline_checkbox", property: "include_newline", isCheckbox: true, isGlobalSetting: true },
{ id: "custom_stopping_strings", property: "custom_stopping_strings", isCheckbox: false, isGlobalSetting: true },
{ id: "custom_stopping_strings_macro", property: "custom_stopping_strings_macro", isCheckbox: true, isGlobalSetting: true }
];
let browser_has_focus = true; let browser_has_focus = true;
const debug_functions = []; const debug_functions = [];
@ -398,6 +415,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() {
@ -535,7 +553,7 @@ function applyChatWidth(type) {
}) })
} }
$('#chat_width_slider_counter').text(power_user.chat_width); $('#chat_width_slider_counter').val(power_user.chat_width);
} }
async function applyThemeColor(type) { async function applyThemeColor(type) {
@ -596,7 +614,7 @@ async function applyCustomCSS() {
async function applyBlurStrength() { async function applyBlurStrength() {
power_user.blur_strength = Number(localStorage.getItem(storage_keys.blur_strength) ?? 1); power_user.blur_strength = Number(localStorage.getItem(storage_keys.blur_strength) ?? 1);
document.documentElement.style.setProperty('--blurStrength', power_user.blur_strength); document.documentElement.style.setProperty('--blurStrength', power_user.blur_strength);
$("#blur_strength_counter").text(power_user.blur_strength); $("#blur_strength_counter").val(power_user.blur_strength);
$("#blur_strength").val(power_user.blur_strength); $("#blur_strength").val(power_user.blur_strength);
@ -605,7 +623,7 @@ async function applyBlurStrength() {
async function applyShadowWidth() { async function applyShadowWidth() {
power_user.shadow_width = Number(localStorage.getItem(storage_keys.shadow_width) ?? 2); power_user.shadow_width = Number(localStorage.getItem(storage_keys.shadow_width) ?? 2);
document.documentElement.style.setProperty('--shadowWidth', power_user.shadow_width); document.documentElement.style.setProperty('--shadowWidth', power_user.shadow_width);
$("#shadow_width_counter").text(power_user.shadow_width); $("#shadow_width_counter").val(power_user.shadow_width);
$("#shadow_width").val(power_user.shadow_width); $("#shadow_width").val(power_user.shadow_width);
} }
@ -623,7 +641,7 @@ async function applyFontScale(type) {
}) })
} }
$("#font_scale_counter").text(power_user.font_scale); $("#font_scale_counter").val(power_user.font_scale);
$("#font_scale").val(power_user.font_scale); $("#font_scale").val(power_user.font_scale);
} }
@ -835,6 +853,18 @@ switchMesIDDisplay();
switchTokenCount(); switchTokenCount();
switchMessageActions(); switchMessageActions();
function getExampleMessagesBehavior() {
if (power_user.strip_examples) {
return 'strip';
}
if (power_user.pin_examples) {
return 'keep';
}
return 'normal';
}
function loadPowerUserSettings(settings, data) { function loadPowerUserSettings(settings, data) {
// Load from settings.json // Load from settings.json
if (settings.power_user !== undefined) { if (settings.power_user !== undefined) {
@ -863,7 +893,6 @@ function loadPowerUserSettings(settings, data) {
const timestamps = localStorage.getItem(storage_keys.timestamps_enabled); const timestamps = localStorage.getItem(storage_keys.timestamps_enabled);
const mesIDDisplay = localStorage.getItem(storage_keys.mesIDDisplay_enabled); const mesIDDisplay = localStorage.getItem(storage_keys.mesIDDisplay_enabled);
const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions); const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions);
console.log(expandMessageActions)
power_user.fast_ui_mode = fastUi === null ? true : fastUi == "true"; power_user.fast_ui_mode = fastUi === null ? true : fastUi == "true";
power_user.movingUI = movingUI === null ? false : movingUI == "true"; power_user.movingUI = movingUI === null ? false : movingUI == "true";
power_user.noShadows = noShadows === null ? false : noShadows == "true"; power_user.noShadows = noShadows === null ? false : noShadows == "true";
@ -872,7 +901,6 @@ function loadPowerUserSettings(settings, data) {
power_user.timestamps_enabled = timestamps === null ? true : timestamps == "true"; power_user.timestamps_enabled = timestamps === null ? true : timestamps == "true";
power_user.mesIDDisplay_enabled = mesIDDisplay === null ? true : mesIDDisplay == "true"; power_user.mesIDDisplay_enabled = mesIDDisplay === null ? true : mesIDDisplay == "true";
power_user.expand_message_actions = expandMessageActions === null ? true : expandMessageActions == "true"; power_user.expand_message_actions = expandMessageActions === null ? true : expandMessageActions == "true";
console.log(power_user.expand_message_actions)
power_user.avatar_style = Number(localStorage.getItem(storage_keys.avatar_style) ?? avatar_styles.ROUND); power_user.avatar_style = Number(localStorage.getItem(storage_keys.avatar_style) ?? avatar_styles.ROUND);
//power_user.chat_display = Number(localStorage.getItem(storage_keys.chat_display) ?? chat_styles.DEFAULT); //power_user.chat_display = Number(localStorage.getItem(storage_keys.chat_display) ?? chat_styles.DEFAULT);
power_user.chat_width = Number(localStorage.getItem(storage_keys.chat_width) ?? 50); power_user.chat_width = Number(localStorage.getItem(storage_keys.chat_width) ?? 50);
@ -896,10 +924,12 @@ function loadPowerUserSettings(settings, data) {
} }
$('#relaxed_api_urls').prop("checked", power_user.relaxed_api_urls); $('#relaxed_api_urls').prop("checked", power_user.relaxed_api_urls);
$('#world_import_dialog').prop("checked", power_user.world_import_dialog);
$('#trim_spaces').prop("checked", power_user.trim_spaces); $('#trim_spaces').prop("checked", power_user.trim_spaces);
$('#continue_on_send').prop("checked", power_user.continue_on_send); $('#continue_on_send').prop("checked", power_user.continue_on_send);
$('#quick_continue').prop("checked", power_user.quick_continue); $('#quick_continue').prop("checked", power_user.quick_continue);
$('#mes_continue').css('display', power_user.quick_continue ? '' : 'none'); $('#mes_continue').css('display', power_user.quick_continue ? '' : 'none');
$('#gestures-checkbox').prop("checked", power_user.gestures);
$('#auto_swipe').prop("checked", power_user.auto_swipe); $('#auto_swipe').prop("checked", power_user.auto_swipe);
$('#auto_swipe_minimum_length').val(power_user.auto_swipe_minimum_length); $('#auto_swipe_minimum_length').val(power_user.auto_swipe_minimum_length);
$('#auto_swipe_blacklist').val(power_user.auto_swipe_blacklist.join(", ")); $('#auto_swipe_blacklist').val(power_user.auto_swipe_blacklist.join(", "));
@ -909,6 +939,8 @@ function loadPowerUserSettings(settings, data) {
$('#fuzzy_search_checkbox').prop("checked", power_user.fuzzy_search); $('#fuzzy_search_checkbox').prop("checked", power_user.fuzzy_search);
$('#persona_show_notifications').prop("checked", power_user.persona_show_notifications); $('#persona_show_notifications').prop("checked", power_user.persona_show_notifications);
$('#encode_tags').prop("checked", power_user.encode_tags); $('#encode_tags').prop("checked", power_user.encode_tags);
$('#example_messages_behavior').val(getExampleMessagesBehavior());
$(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop("selected", true);
$("#console_log_prompts").prop("checked", power_user.console_log_prompts); $("#console_log_prompts").prop("checked", power_user.console_log_prompts);
$('#auto_fix_generated_markdown').prop("checked", power_user.auto_fix_generated_markdown); $('#auto_fix_generated_markdown').prop("checked", power_user.auto_fix_generated_markdown);
@ -919,8 +951,6 @@ function loadPowerUserSettings(settings, data) {
$("#confirm_message_delete").prop("checked", power_user.confirm_message_delete !== undefined ? !!power_user.confirm_message_delete : true); $("#confirm_message_delete").prop("checked", power_user.confirm_message_delete !== undefined ? !!power_user.confirm_message_delete : true);
$("#spoiler_free_mode").prop("checked", power_user.spoiler_free_mode); $("#spoiler_free_mode").prop("checked", power_user.spoiler_free_mode);
$("#collapse-newlines-checkbox").prop("checked", power_user.collapse_newlines); $("#collapse-newlines-checkbox").prop("checked", power_user.collapse_newlines);
$("#pin-examples-checkbox").prop("checked", power_user.pin_examples);
$("#remove-examples-checkbox").prop("checked", power_user.strip_examples);
$("#always-force-name2-checkbox").prop("checked", power_user.always_force_name2); $("#always-force-name2-checkbox").prop("checked", power_user.always_force_name2);
$("#trim_sentences_checkbox").prop("checked", power_user.trim_sentences); $("#trim_sentences_checkbox").prop("checked", power_user.trim_sentences);
$("#include_newline_checkbox").prop("checked", power_user.include_newline); $("#include_newline_checkbox").prop("checked", power_user.include_newline);
@ -957,13 +987,13 @@ function loadPowerUserSettings(settings, data) {
$("#token_padding").val(power_user.token_padding); $("#token_padding").val(power_user.token_padding);
$("#font_scale").val(power_user.font_scale); $("#font_scale").val(power_user.font_scale);
$("#font_scale_counter").text(power_user.font_scale); $("#font_scale_counter").val(power_user.font_scale);
$("#blur_strength").val(power_user.blur_strength); $("#blur_strength").val(power_user.blur_strength);
$("#blur_strength_counter").text(power_user.blur_strength); $("#blur_strength_counter").val(power_user.blur_strength);
$("#shadow_width").val(power_user.shadow_width); $("#shadow_width").val(power_user.shadow_width);
$("#shadow_width_counter").text(power_user.shadow_width); $("#shadow_width_counter").val(power_user.shadow_width);
$("#main-text-color-picker").attr('color', power_user.main_text_color); $("#main-text-color-picker").attr('color', power_user.main_text_color);
$("#italics-color-picker").attr('color', power_user.italics_text_color); $("#italics-color-picker").attr('color', power_user.italics_text_color);
@ -1057,9 +1087,13 @@ function loadMaxContextUnlocked() {
function switchMaxContextSize() { function switchMaxContextSize() {
const elements = [$('#max_context'), $('#rep_pen_range'), $('#rep_pen_range_textgenerationwebui')]; const elements = [$('#max_context'), $('#rep_pen_range'), $('#rep_pen_range_textgenerationwebui')];
const maxValue = power_user.max_context_unlocked ? MAX_CONTEXT_UNLOCKED : MAX_CONTEXT_DEFAULT; const maxValue = power_user.max_context_unlocked ? MAX_CONTEXT_UNLOCKED : MAX_CONTEXT_DEFAULT;
const minValue = power_user.max_context_unlocked ? unlockedMaxContestMin : 0;
const steps = power_user.max_context_unlocked ? unlockedMaxContextStep : 256;
for (const element of elements) { for (const element of elements) {
element.attr('max', maxValue); element.attr('max', maxValue);
element.attr('step', steps);
element.attr('min', minValue);
const value = Number(element.val()); const value = Number(element.val());
if (value >= maxValue) { if (value >= maxValue) {
@ -1068,24 +1102,50 @@ function switchMaxContextSize() {
} }
} }
function loadContextSettings() { // Fetch a compiled object of all preset settings
const controls = [ function getContextSettings() {
{ id: "context_story_string", property: "story_string", isCheckbox: false }, let compiledSettings = {};
{ id: "context_example_separator", property: "example_separator", isCheckbox: false },
{ id: "context_chat_start", property: "chat_start", isCheckbox: false },
];
controls.forEach(control => { contextControls.forEach((control) => {
let value = control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property];
// Force to a boolean if the setting is a checkbox
if (control.isCheckbox) {
value = !!value;
}
compiledSettings[control.property] = value;
});
return compiledSettings;
}
// TODO: Maybe add a refresh button to reset settings to preset
// TODO: Add "global state" if a preset doesn't set the power_user checkboxes
function loadContextSettings() {
contextControls.forEach(control => {
const $element = $(`#${control.id}`); const $element = $(`#${control.id}`);
if (control.isGlobalSetting) {
return;
}
if (control.isCheckbox) { if (control.isCheckbox) {
$element.prop('checked', power_user.context[control.property]); $element.prop('checked', power_user.context[control.property]);
} else { } else {
$element.val(power_user.context[control.property]); $element.val(power_user.context[control.property]);
} }
// If the setting already exists, no need to duplicate it
// TODO: Maybe check the power_user object for the setting instead of a flag?
$element.on('input', function () { $element.on('input', function () {
power_user.context[control.property] = control.isCheckbox ? !!$(this).prop('checked') : $(this).val(); const value = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
if (control.isGlobalSetting) {
power_user[control.property] = value;
} else {
power_user.context[control.property] = value;
}
saveSettingsDebounced(); saveSettingsDebounced();
if (!control.isCheckbox) { if (!control.isCheckbox) {
resetScrollHeight($element); resetScrollHeight($element);
@ -1111,15 +1171,24 @@ function loadContextSettings() {
} }
power_user.context.preset = name; power_user.context.preset = name;
controls.forEach(control => { contextControls.forEach(control => {
if (preset[control.property] !== undefined) { if (preset[control.property] !== undefined) {
power_user.context[control.property] = preset[control.property]; if (control.isGlobalSetting) {
power_user[control.property] = preset[control.property];
} else {
power_user.context[control.property] = preset[control.property];
}
const $element = $(`#${control.id}`); const $element = $(`#${control.id}`);
if (control.isCheckbox) { if (control.isCheckbox) {
$element.prop('checked', power_user.context[control.property]).trigger('input'); $element
.prop('checked', control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property])
.trigger('input');
} else { } else {
$element.val(power_user.context[control.property]).trigger('input'); $element
.val(control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property])
.trigger('input');
} }
} }
}); });
@ -1162,7 +1231,7 @@ function highlightDefaultContext() {
export function fuzzySearchCharacters(searchValue) { export function fuzzySearchCharacters(searchValue) {
const fuse = new Fuse(characters, { const fuse = new Fuse(characters, {
keys: [ keys: [
{ name: 'data.name', weight: 5 }, { name: 'data.name', weight: 8 },
{ name: 'data.description', weight: 3 }, { name: 'data.description', weight: 3 },
{ name: 'data.mes_example', weight: 3 }, { name: 'data.mes_example', weight: 3 },
{ name: 'data.scenario', weight: 2 }, { name: 'data.scenario', weight: 2 },
@ -1240,7 +1309,7 @@ export function renderStoryString(params) {
output = output.trimStart(); output = output.trimStart();
// add a newline to the end of the story string if it doesn't have one // add a newline to the end of the story string if it doesn't have one
if (!output.endsWith('\n')) { if (output.length > 0 && !output.endsWith('\n')) {
output += '\n'; output += '\n';
} }
@ -1410,12 +1479,15 @@ async function resetMovablePanels(type) {
'WorldInfo', 'WorldInfo',
'floatingPrompt', 'floatingPrompt',
'expression-holder', 'expression-holder',
'groupMemberListPopout' 'groupMemberListPopout',
'summaryExtensionPopout',
'gallery'
]; ];
const panelStyles = ['top', 'left', 'right', 'bottom', 'height', 'width', 'margin',]; const panelStyles = ['top', 'left', 'right', 'bottom', 'height', 'width', 'margin',];
panelIds.forEach((id) => { panelIds.forEach((id) => {
console.log(id)
const panel = document.getElementById(id); const panel = document.getElementById(id);
if (panel) { if (panel) {
@ -1844,28 +1916,6 @@ $(document).ready(() => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$("#pin-examples-checkbox").change(function () {
if ($(this).prop("checked")) {
$("#remove-examples-checkbox").prop("checked", false).prop("disabled", true);
power_user.strip_examples = false;
} else {
$("#remove-examples-checkbox").prop("disabled", false);
}
power_user.pin_examples = !!$(this).prop("checked");
saveSettingsDebounced();
});
$("#remove-examples-checkbox").change(function () {
if ($(this).prop("checked")) {
$("#pin-examples-checkbox").prop("checked", false).prop("disabled", true);
power_user.pin_examples = false;
} else {
$("#pin-examples-checkbox").prop("disabled", false);
}
power_user.strip_examples = !!$(this).prop("checked");
saveSettingsDebounced();
});
// include newline is the child of trim sentences // include newline is the child of trim sentences
// if include newline is checked, trim sentences must be checked // if include newline is checked, trim sentences must be checked
// if trim sentences is unchecked, include newline must be unchecked // if trim sentences is unchecked, include newline must be unchecked
@ -1924,6 +1974,31 @@ $(document).ready(() => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#example_messages_behavior').on('change', function () {
const selectedOption = String($(this).find(':selected').val());
console.log('Setting example messages behavior to', selectedOption);
switch (selectedOption) {
case 'normal':
power_user.pin_examples = false;
power_user.strip_examples = false;
break;
case 'keep':
power_user.pin_examples = true;
power_user.strip_examples = false;
break;
case 'strip':
power_user.pin_examples = false;
power_user.strip_examples = true;
break;
}
console.debug('power_user.pin_examples', power_user.pin_examples);
console.debug('power_user.strip_examples', power_user.strip_examples);
saveSettingsDebounced();
});
// Settings that go to local storage // Settings that go to local storage
$("#fast_ui_mode").change(function () { $("#fast_ui_mode").change(function () {
power_user.fast_ui_mode = $(this).prop("checked"); power_user.fast_ui_mode = $(this).prop("checked");
@ -1987,7 +2062,7 @@ $(document).ready(() => {
$(`input[name="font_scale"]`).on('input', async function (e) { $(`input[name="font_scale"]`).on('input', async function (e) {
power_user.font_scale = Number(e.target.value); power_user.font_scale = Number(e.target.value);
$("#font_scale_counter").text(power_user.font_scale); $("#font_scale_counter").val(power_user.font_scale);
localStorage.setItem(storage_keys.font_scale, power_user.font_scale); localStorage.setItem(storage_keys.font_scale, power_user.font_scale);
await applyFontScale(); await applyFontScale();
saveSettingsDebounced(); saveSettingsDebounced();
@ -1995,7 +2070,7 @@ $(document).ready(() => {
$(`input[name="blur_strength"]`).on('input', async function (e) { $(`input[name="blur_strength"]`).on('input', async function (e) {
power_user.blur_strength = Number(e.target.value); power_user.blur_strength = Number(e.target.value);
$("#blur_strength_counter").text(power_user.blur_strength); $("#blur_strength_counter").val(power_user.blur_strength);
localStorage.setItem(storage_keys.blur_strength, power_user.blur_strength); localStorage.setItem(storage_keys.blur_strength, power_user.blur_strength);
await applyBlurStrength(); await applyBlurStrength();
saveSettingsDebounced(); saveSettingsDebounced();
@ -2003,7 +2078,7 @@ $(document).ready(() => {
$(`input[name="shadow_width"]`).on('input', async function (e) { $(`input[name="shadow_width"]`).on('input', async function (e) {
power_user.shadow_width = Number(e.target.value); power_user.shadow_width = Number(e.target.value);
$("#shadow_width_counter").text(power_user.shadow_width); $("#shadow_width_counter").val(power_user.shadow_width);
localStorage.setItem(storage_keys.shadow_width, power_user.shadow_width); localStorage.setItem(storage_keys.shadow_width, power_user.shadow_width);
await applyShadowWidth(); await applyShadowWidth();
saveSettingsDebounced(); saveSettingsDebounced();
@ -2117,6 +2192,11 @@ $(document).ready(() => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#gestures-checkbox').on('change', function () {
power_user.gestures = !!$('#gestures-checkbox').prop('checked');
saveSettingsDebounced();
});
$('#auto_swipe').on('input', function () { $('#auto_swipe').on('input', function () {
power_user.auto_swipe = !!$(this).prop('checked'); power_user.auto_swipe = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
@ -2306,6 +2386,12 @@ $(document).ready(() => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$("#world_import_dialog").on("input", function () {
const value = !!$(this).prop('checked');
power_user.world_import_dialog = value;
saveSettingsDebounced();
});
$('#spoiler_free_mode').on('input', function () { $('#spoiler_free_mode').on('input', function () {
power_user.spoiler_free_mode = !!$(this).prop('checked'); power_user.spoiler_free_mode = !!$(this).prop('checked');
switchSpoilerMode(); switchSpoilerMode();

View File

@ -18,7 +18,7 @@ import {
import { groups, selected_group } from "./group-chats.js"; import { groups, selected_group } from "./group-chats.js";
import { instruct_presets } from "./instruct-mode.js"; import { instruct_presets } from "./instruct-mode.js";
import { kai_settings } from "./kai-settings.js"; import { kai_settings } from "./kai-settings.js";
import { context_presets, power_user } from "./power-user.js"; import { context_presets, getContextSettings, power_user } from "./power-user.js";
import { import {
textgenerationwebui_preset_names, textgenerationwebui_preset_names,
textgenerationwebui_presets, textgenerationwebui_presets,
@ -104,6 +104,7 @@ class PresetManager {
async updatePreset() { async updatePreset() {
const selected = $(this.select).find("option:selected"); const selected = $(this.select).find("option:selected");
console.log(selected)
if (selected.val() == 'gui') { if (selected.val() == 'gui') {
toastr.info('Cannot update GUI preset'); toastr.info('Cannot update GUI preset');
@ -236,7 +237,7 @@ class PresetManager {
case "textgenerationwebui": case "textgenerationwebui":
return textgenerationwebui_settings; return textgenerationwebui_settings;
case "context": case "context":
const context_preset = structuredClone(power_user.context); const context_preset = getContextSettings();
context_preset['name'] = name || power_user.context.preset; context_preset['name'] = name || power_user.context.preset;
return context_preset; return context_preset;
case "instruct": case "instruct":
@ -382,8 +383,10 @@ jQuery(async () => {
return; return;
} }
const name = file.name.replace('.json', '').replace('.settings', ''); const fileName = file.name.replace('.json', '').replace('.settings', '');
const data = await parseJsonFile(file); const data = await parseJsonFile(file);
const name = data?.name ?? fileName;
data['name'] = name;
await presetManager.savePreset(name, data); await presetManager.savePreset(name, data);
toastr.success('Preset imported'); toastr.success('Preset imported');

View File

@ -1,6 +1,6 @@
import { saveSettingsDebounced } from "../script.js"; import { saveSettingsDebounced } from "../script.js";
import { power_user } from "./power-user.js"; import { power_user } from "./power-user.js";
import { isUrlOrAPIKey } from "./utils.js"; import { isValidUrl } from "./utils.js";
/** /**
* @param {{ term: string; }} request * @param {{ term: string; }} request
@ -64,7 +64,7 @@ function onServerConnectClick() {
const value = String($(`[data-server-history="${serverLabel}"]`).val()).toLowerCase().trim(); const value = String($(`[data-server-history="${serverLabel}"]`).val()).toLowerCase().trim();
// Don't save empty values or invalid URLs // Don't save empty values or invalid URLs
if (!value || !isUrlOrAPIKey(value)) { if (!value || !isValidUrl(value)) {
return; return;
} }

View File

@ -1,29 +1,29 @@
export { MODULE_NAME }; /**
* Search for settings that match the search string and highlight them.
const MODULE_NAME = 'settingsSearch'; */
async function addSettingsSearchHTML() {
const html = `
<div class="wide100p">
<div class="justifyLeft">
<textarea id="settingsSearch" class="wide100p textarea_compact" rows="1" placeholder="Search Settings"></textarea>
</div>
</div>`
$("#user-settings-block").prepend(html);
}
async function searchSettings() { async function searchSettings() {
removeHighlighting(); // Remove previous highlights removeHighlighting(); // Remove previous highlights
let searchString = $("#settingsSearch").val(); const searchString = String($("#settingsSearch").val());
let searchableText = $("#user-settings-block-content"); // Get the HTML block const searchableText = $("#user-settings-block-content"); // Get the HTML block
if (searchString.trim() !== "") { if (searchString.trim() !== "") {
highlightMatchingElements(searchableText[0], searchString); // Highlight matching elements highlightMatchingElements(searchableText[0], searchString); // Highlight matching elements
} }
} }
/**
* Check if the element is a child of a header element
* @param {HTMLElement | Text | Document | Comment} element Settings block HTML element
* @returns {boolean} True if the element is a child of a header element, false otherwise
*/
function isParentHeader(element) { function isParentHeader(element) {
return $(element).closest('h4, h3').length > 0; return $(element).closest('h4, h3').length > 0;
} }
/**
* Recursively highlight elements that match the search string
* @param {HTMLElement | Text | Document | Comment} element Settings block HTML element
* @param {string} searchString Search string
*/
function highlightMatchingElements(element, searchString) { function highlightMatchingElements(element, searchString) {
$(element).contents().each(function () { $(element).contents().each(function () {
const isTextNode = this.nodeType === Node.TEXT_NODE; const isTextNode = this.nodeType === Node.TEXT_NODE;
@ -41,17 +41,14 @@ function highlightMatchingElements(element, searchString) {
} }
}); });
} }
/**
* Remove highlighting from previously highlighted elements.
*/
function removeHighlighting() { function removeHighlighting() {
$(".highlighted").removeClass("highlighted"); // Remove CSS class from previously highlighted elements $(".highlighted").removeClass("highlighted"); // Remove CSS class from previously highlighted elements
} }
jQuery(() => { jQuery(() => {
//addSettingsSearchHTML();
$('#settingsSearch').on('input change', searchSettings); $('#settingsSearch').on('input change', searchSettings);
}); });

View File

@ -21,9 +21,12 @@ import {
reloadCurrentChat, reloadCurrentChat,
sendMessageAsUser, sendMessageAsUser,
name1, name1,
Generate,
this_chid,
setCharacterName,
} from "../script.js"; } from "../script.js";
import { getMessageTimeStamp } from "./RossAscends-mods.js"; import { getMessageTimeStamp } from "./RossAscends-mods.js";
import { resetSelectedGroup } from "./group-chats.js"; import { resetSelectedGroup, selected_group } from "./group-chats.js";
import { getRegexedString, regex_placement } from "./extensions/regex/engine.js"; import { getRegexedString, regex_placement } from "./extensions/regex/engine.js";
import { chat_styles, power_user } from "./power-user.js"; import { chat_styles, power_user } from "./power-user.js";
import { autoSelectPersona } from "./personas.js"; import { autoSelectPersona } from "./personas.js";
@ -105,7 +108,10 @@ class SlashCommandParser {
getHelpString() { getHelpString() {
const listItems = this.helpStrings.map(x => `<li>${x}</li>`).join('\n'); const listItems = this.helpStrings.map(x => `<li>${x}</li>`).join('\n');
return `<p>Slash commands:</p><ol>${listItems}</ol>`; return `<p>Slash commands:</p><ol>${listItems}</ol>
<small>Slash commands can be batched into a single input by adding a pipe character | at the end, and then writing a new slash command.</small>
<ul><li><small>Example:</small><code>/cut 1 | /sys Hello, | /continue</code></li>
<li>This will remove the first message in chat, send a system message that starts with 'Hello,', and then ask the AI to continue the message.</li></ul>`;
} }
} }
@ -128,6 +134,7 @@ parser.addCommand('flat', setFlatModeCallback, ['default'], ' sets the messa
parser.addCommand('continue', continueChatCallback, ['cont'], ' continues the last message in the chat', true, true); parser.addCommand('continue', continueChatCallback, ['cont'], ' continues the last message in the chat', true, true);
parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace">(name)</span> opens up a chat with the character by its name', true, true); parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace">(name)</span> opens up a chat with the character by its name', true, true);
parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">(prompt)</span> generates a system message using a specified prompt', true, true); parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">(prompt)</span> generates a system message using a specified prompt', true, true);
parser.addCommand('ask', askCharacter, [], '<span class="monospace">(prompt)</span> asks a specified character card a prompt', true, true);
parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '<span class="monospace">(name)</span> deletes all messages attributed to a specified name', true, true); parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '<span class="monospace">(name)</span> deletes all messages attributed to a specified name', true, true);
parser.addCommand('send', sendUserMessageCallback, ['add'], '<span class="monospace">(text)</span> adds a user message to the chat log without triggering a generation', true, true); parser.addCommand('send', sendUserMessageCallback, ['add'], '<span class="monospace">(text)</span> adds a user message to the chat log without triggering a generation', true, true);
@ -135,6 +142,88 @@ const NARRATOR_NAME_KEY = 'narrator_name';
const NARRATOR_NAME_DEFAULT = 'System'; const NARRATOR_NAME_DEFAULT = 'System';
export const COMMENT_NAME_DEFAULT = 'Note'; export const COMMENT_NAME_DEFAULT = 'Note';
async function askCharacter(_, text) {
// Prevent generate recursion
$('#send_textarea').val('');
// Not supported in group chats
// TODO: Maybe support group chats?
if (selected_group) {
toastr.error("Cannot run this command in a group chat!");
return;
}
if (!text) {
console.warn('WARN: No text provided for /ask command')
}
const parts = text.split('\n');
if (parts.length <= 1) {
toastr.warning('Both character name and message are required. Separate them with a new line.');
return;
}
// Grabbing the message
const name = parts.shift().trim();
let mesText = parts.join('\n').trim();
const prevChId = this_chid;
// Find the character
const chId = characters.findIndex((e) => e.name === name);
if (!characters[chId] || chId === -1) {
toastr.error("Character not found.");
return;
}
// Override character and send a user message
setCharacterId(chId);
// TODO: Maybe look up by filename instead of name
const character = characters[chId];
let force_avatar, original_avatar;
if (character && character.avatar !== 'none') {
force_avatar = getThumbnailUrl('avatar', character.avatar);
original_avatar = character.avatar;
}
else {
force_avatar = default_avatar;
original_avatar = default_avatar;
}
setCharacterName(character.name);
sendMessageAsUser(mesText)
const restoreCharacter = () => {
setCharacterId(prevChId);
setCharacterName(characters[prevChId].name);
// Only force the new avatar if the character name is the same
// This skips if an error was fired
const lastMessage = chat[chat.length - 1];
if (lastMessage && lastMessage?.name === character.name) {
lastMessage.force_avatar = force_avatar;
lastMessage.original_avatar = original_avatar;
}
// Kill this callback once the event fires
eventSource.removeListener(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter)
}
// Run generate and restore previous character on error
try {
toastr.info(`Asking ${character.name} something...`);
await Generate('ask_command')
} catch {
restoreCharacter()
}
// Restore previous character once message renders
// Hack for generate
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter);
}
async function sendUserMessageCallback(_, text) { async function sendUserMessageCallback(_, text) {
if (!text) { if (!text) {
console.warn('WARN: No text provided for /send command'); console.warn('WARN: No text provided for /send command');

Some files were not shown because too many files have changed in this diff Show More