Merge branch 'SillyTavern:staging' into staging

This commit is contained in:
Tony Ribeiro 2023-08-23 01:44:52 +02:00 committed by GitHub
commit 5009416f94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 2362 additions and 1336 deletions

View File

@ -135,7 +135,7 @@
"instruct": { "instruct": {
"enabled": false, "enabled": false,
"preset": "🧙 Roleplay", "preset": "🧙 Roleplay",
"system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\nAvoid repetition, don't loop. Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.\n", "system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\nAvoid repetition, don't loop. Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.\n\n### Input:",
"input_sequence": "\n### Instruction:", "input_sequence": "\n### Instruction:",
"output_sequence": "\n### Response:", "output_sequence": "\n### Response:",
"last_output_sequence": "\n### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):", "last_output_sequence": "\n### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):",

12
package-lock.json generated
View File

@ -53,8 +53,7 @@
}, },
"devDependencies": { "devDependencies": {
"pkg": "^5.8.1", "pkg": "^5.8.1",
"pkg-fetch": "^3.5.2", "pkg-fetch": "^3.5.2"
"toastr": "^2.1.4"
} }
}, },
"node_modules/@agnai/sentencepiece-js": { "node_modules/@agnai/sentencepiece-js": {
@ -3366,15 +3365,6 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toastr": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz",
"integrity": "sha512-LIy77F5n+sz4tefMmFOntcJ6HL0Fv3k1TDnNmFZ0bU/GcvIIfy6eG2v7zQmMiYgaalAiUv75ttFrPn5s0gyqlA==",
"dev": true,
"dependencies": {
"jquery": ">=1.12.0"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@ -81,7 +81,6 @@
}, },
"devDependencies": { "devDependencies": {
"pkg": "^5.8.1", "pkg": "^5.8.1",
"pkg-fetch": "^3.5.2", "pkg-fetch": "^3.5.2"
"toastr": "^2.1.4"
} }
} }

View File

@ -63,42 +63,8 @@
<link rel="stylesheet" href="css/bg_load.css"> <link rel="stylesheet" href="css/bg_load.css">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<script> <script type="module" src="scripts/i18n.js"></script>
function applyLocale() { <script type="module" src="script.js"></script>
const overrideLanguage = localStorage.getItem("language");
var language = overrideLanguage || navigator.language || navigator.userLanguage;
language = language.toLowerCase();
console.log(language)
//load the appropriate language file
$.getJSON("i18n.json", function (data) {
console.log(data)
if (data.lang.indexOf(language) < 0) language = "en";
console.log(language)
//find all the elements with `data-i18n` attribute
$("[data-i18n]").each(function () {
//read the translation from the language data
const keys = $(this).data("i18n").split(';'); // Multi-key entries are ; delimited
for (const key of keys) {
const attrmatch = key.match(/\[(\S+)\](.+)/); // [attribute]key
if (attrmatch) { // attribute-tagged key
const locval = data?.[language]?.[attrmatch[2]];
if (locval) {
$(this).attr(attrmatch[1], locval);
}
} else { // No attribute tag, treat as 'text'
const locval = data?.[language]?.[key];
if (locval) {
$(this).text(locval);
}
}
}
});
});
}
$(document).ready(applyLocale);
window["applyLocale"] = applyLocale;
</script>
<script type=module src="script.js"></script>
<script type="module" src="scripts/world-info.js"></script> <script type="module" src="scripts/world-info.js"></script>
<script type="module" src="scripts/group-chats.js"></script> <script type="module" src="scripts/group-chats.js"></script>
@ -669,7 +635,7 @@
Max prompt cost: <span id="openrouter_max_prompt_cost">Unknown</span> Max prompt cost: <span id="openrouter_max_prompt_cost">Unknown</span>
</div> </div>
<hr> <hr>
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21"> <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale">
<div class="range-block-title" data-i18n="Temperature"> <div class="range-block-title" data-i18n="Temperature">
Temperature Temperature
</div> </div>
@ -744,7 +710,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="range-block" data-source="openai,claude,openrouter,ai21"> <div class="range-block" data-source="openai,claude,openrouter,ai21,scale">
<div class="range-block-title" data-i18n="Top-p"> <div class="range-block-title" data-i18n="Top-p">
Top P Top P
</div> </div>
@ -1470,9 +1436,22 @@
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<div id="quick-edit-container"> <div class="range-block m-t-1">
<div class="range-block"> <div class="justifyLeft" data-i18n="Main">Main</div>
<span data-i18n="Select a character to show quick edit options.">Select a character to show quick edit options.</span> <div class="wide100p">
<textarea id="main_prompt_quick_edit_textarea" class="text_pole textarea_compact" rows="6" placeholder="" data-pm-prompt="main"></textarea>
</div>
</div>
<div class="range-block m-t-1">
<div class="justifyLeft" data-i18n="NSFW">NSFW</div>
<div class="wide100p">
<textarea id="nsfw_prompt_quick_edit_textarea" class="text_pole textarea_compact" rows="6" placeholder="" data-pm-prompt="nsfw"></textarea>
</div>
</div>
<div class="range-block m-t-1">
<div class="justifyLeft" data-i18n="Jailbreak">Jailbreak</div>
<div class="wide100p">
<textarea id="jailbreak_prompt_quick_edit_textarea" class="text_pole textarea_compact" rows="6" placeholder="" data-pm-prompt="jailbreak"></textarea>
</div> </div>
</div> </div>
<div id="claude_assistant_prefill_block" data-source="claude" class="range-block"> <div id="claude_assistant_prefill_block" data-source="claude" class="range-block">
@ -1612,7 +1591,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="range-block m-t-1" data-source="openai,openrouter"> <div class="range-block m-t-1" data-source="openai,openrouter,scale">
<div class="range-block-title openai_restorable" data-i18n="Logit Bias"> <div class="range-block-title openai_restorable" data-i18n="Logit Bias">
Logit Bias Logit Bias
</div> </div>
@ -2010,6 +1989,7 @@
</form> </form>
<form id="scale_form" data-source="scale" action="javascript:void(null);" method="post" enctype="multipart/form-data"> <form id="scale_form" data-source="scale" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<div id="normal_scale_form">
<h4>Scale API Key</h4> <h4>Scale API Key</h4>
<div class="flex-container"> <div class="flex-container">
<input id="api_key_scale" name="api_key_scale" class="text_pole flex1" maxlength="500" value="" autocomplete="off"> <input id="api_key_scale" name="api_key_scale" class="text_pole flex1" maxlength="500" value="" autocomplete="off">
@ -2020,8 +2000,23 @@
</div> </div>
<h4>Scale API URL</h4> <h4>Scale API URL</h4>
<input id="api_url_scale" name="api_url_scale" class="text_pole" maxlength="500" value="" autocomplete="off" placeholder="https://dashboard.scale.com/spellbook/api/v2/deploy/xxxxxxx"> <input id="api_url_scale" name="api_url_scale" class="text_pole" maxlength="500" value="" autocomplete="off" placeholder="https://dashboard.scale.com/spellbook/api/v2/deploy/xxxxxxx">
</div>
<div id="alt_scale_form">
<h4>Scale Cookie (_jwt)</h4>
<div class="flex-container">
<input id="scale_cookie" name="scale_cookie" class="text_pole flex1" maxlength="500" value="" autocomplete="off">
<div title="Clear your cookie" data-i18n="[title]Clear your cookie" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="scale_cookie"></div>
</div>
<div data-for="scale_cookie" class="neutral_warning">
For privacy reasons, your cookie will be hidden after you reload the page.
</div>
</div>
<!-- Its only purpose is to trigger max context size check --> <!-- Its only purpose is to trigger max context size check -->
<select id="model_scale_select" class="displayNone"></select> <select id="model_scale_select" class="displayNone"></select>
<label for="scale-alt" class="checkbox_label">
<input id="scale-alt" type="checkbox" checked>
<span data-i18n="Alt Method">Alt Method</span>
</label>
</form> </form>
<form id="ai21_form" data-source="ai21" action="javascript:void(null);" method="post" enctype="multipart/form-data"> <form id="ai21_form" data-source="ai21" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@ -2999,7 +2994,10 @@
</div> </div>
<div> <div>
<h4 data-i18n="Persona Description">Persona Description</h4> <h4 data-i18n="Persona Description">Persona Description</h4>
<textarea id="persona_description" name="persona_description" placeholder="Example:&#10;[{{user}} is a 28-year-old Romanian cat girl.]" class="text_pole textarea_compact" maxlength="5000" value="" autocomplete="off" rows="4"></textarea> <textarea id="persona_description" name="persona_description" placeholder="Example:&#10;[{{user}} is a 28-year-old Romanian cat girl.]" class="text_pole textarea_compact" maxlength="5000" value="" autocomplete="off" rows="8"></textarea>
<div class="extension_token_counter">
Tokens: <span id="persona_description_token_count">0</span>
</div>
<label for="persona_description_position" data-i18n="Position:">Position:</label> <label for="persona_description_position" data-i18n="Position:">Position:</label>
<select id="persona_description_position"> <select id="persona_description_position">
<option value="0" data-i18n="In Story String / Chat Completion: Before Character Card">In Story String / Chat Completion: Before Character Card</option> <option value="0" data-i18n="In Story String / Chat Completion: Before Character Card">In Story String / Chat Completion: Before Character Card</option>

View File

@ -1,6 +1,6 @@
{ {
"name": "🧙 Roleplay", "name": "🧙 Roleplay",
"system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\nAvoid repetition, don't loop. Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.\n", "system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\nAvoid repetition, don't loop. Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.\n\n### Input:",
"input_sequence": "\n### Instruction:", "input_sequence": "\n### Instruction:",
"output_sequence": "\n### Response:", "output_sequence": "\n### Response:",
"last_output_sequence": "\n### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):", "last_output_sequence": "\n### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):",

28
public/jsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"checkJs": true,
"target": "ESNext",
"module": "commonjs",
"allowUmdGlobalAccess": true,
"allowSyntheticDefaultImports": true
},
"exclude": [
"node_modules"
],
"typeAcquisition": {
"include": [
"jquery",
"@popperjs/core",
"toastr",
"showdown",
"dompurify",
"moment",
"seedrandom",
"showdown-katex",
"droll",
"handlebars",
"highlight.js",
"localforage"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -513,6 +513,38 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
}); });
} }
// Fill quick edit fields for the first time
if ('global' === this.configuration.promptOrder.strategy) {
const handleQuickEditSave = (event) => {
const promptId = event.target.dataset.pmPrompt;
const prompt = this.getPromptById(promptId);
prompt.content = event.target.value;
// Update edit form if present
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
const popupEditFormPrompt = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
if (popupEditFormPrompt.offsetParent) {
popupEditFormPrompt.value = prompt.content;
}
this.log('Saved prompt: ' + promptId);
this.saveServiceSettings().then(() => this.render());
};
const mainPrompt = this.getPromptById('main');
const mainElementId = this.updateQuickEdit('main', mainPrompt);
document.getElementById(mainElementId).addEventListener('blur', handleQuickEditSave);
const nsfwPrompt = this.getPromptById('nsfw');
const nsfwElementId = this.updateQuickEdit('nsfw', nsfwPrompt);
document.getElementById(nsfwElementId).addEventListener('blur', handleQuickEditSave);
const jailbreakPrompt = this.getPromptById('jailbreak');
const jailbreakElementId = this.updateQuickEdit('jailbreak', jailbreakPrompt);
document.getElementById(jailbreakElementId).addEventListener('blur', handleQuickEditSave);
}
// Re-render when chat history changes. // Re-render when chat history changes.
eventSource.on(event_types.MESSAGE_DELETED, () => this.renderDebounced()); eventSource.on(event_types.MESSAGE_DELETED, () => this.renderDebounced());
eventSource.on(event_types.MESSAGE_EDITED, () => this.renderDebounced()); eventSource.on(event_types.MESSAGE_EDITED, () => this.renderDebounced());
@ -520,6 +552,7 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
// Re-render when chatcompletion settings change // Re-render when chatcompletion settings change
eventSource.on(event_types.CHATCOMPLETION_SOURCE_CHANGED, () => this.renderDebounced()); eventSource.on(event_types.CHATCOMPLETION_SOURCE_CHANGED, () => this.renderDebounced());
eventSource.on(event_types.CHATCOMPLETION_MODEL_CHANGED, () => this.renderDebounced()); eventSource.on(event_types.CHATCOMPLETION_MODEL_CHANGED, () => this.renderDebounced());
// Re-render when the character changes. // Re-render when the character changes.
@ -577,6 +610,15 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
this.hidePopup(); this.hidePopup();
this.clearEditForm(); this.clearEditForm();
this.renderDebounced(); this.renderDebounced();
const mainPrompt = this.getPromptById('main');
this.updateQuickEdit('main', mainPrompt);
const nsfwPrompt = this.getPromptById('nsfw');
this.updateQuickEdit('nsfw', nsfwPrompt);
const jailbreakPrompt = this.getPromptById('jailbreak');
this.updateQuickEdit('jailbreak', jailbreakPrompt);
}); });
}); });
@ -609,6 +651,7 @@ PromptManagerModule.prototype.render = function (afterTryGenerate = true) {
this.makeDraggable(); this.makeDraggable();
this.profileEnd('render'); this.profileEnd('render');
}).catch(error => { }).catch(error => {
this.profileEnd('filling context');
this.log('Error caught during render: ' + error); this.log('Error caught during render: ' + error);
this.renderPromptManager(); this.renderPromptManager();
this.renderPromptManagerListItems() this.renderPromptManagerListItems()
@ -1016,8 +1059,11 @@ PromptManagerModule.prototype.createQuickEdit = function (identifier, title) {
} }
PromptManagerModule.prototype.updateQuickEdit = function (identifier, prompt) { PromptManagerModule.prototype.updateQuickEdit = function (identifier, prompt) {
const textarea = document.getElementById(`${identifier}_prompt_quick_edit_textarea`); const elementId = `${identifier}_prompt_quick_edit_textarea`;
const textarea = document.getElementById(elementId);
textarea.value = prompt.content; textarea.value = prompt.content;
return elementId;
} }
/** /**
@ -1312,48 +1358,9 @@ PromptManagerModule.prototype.renderPromptManager = function () {
footerDiv.querySelector('#prompt-manager-export').addEventListener('click', showExportSelection); footerDiv.querySelector('#prompt-manager-export').addEventListener('click', showExportSelection);
rangeBlockDiv.querySelector('.export-promptmanager-prompts-full').addEventListener('click', this.handleFullExport); rangeBlockDiv.querySelector('.export-promptmanager-prompts-full').addEventListener('click', this.handleFullExport);
rangeBlockDiv.querySelector('.export-promptmanager-prompts-character')?.addEventListener('click', this.handleCharacterExport); rangeBlockDiv.querySelector('.export-promptmanager-prompts-character')?.addEventListener('click', this.handleCharacterExport);
const quickEditContainer = document.getElementById('quick-edit-container');
const heights = this.saveTextAreaHeights(quickEditContainer);
quickEditContainer.innerHTML = '';
this.createQuickEdit('jailbreak', 'Jailbreak');
this.createQuickEdit('nsfw', 'NSFW');
this.createQuickEdit('main', 'Main');
this.restoreTextAreaHeights(quickEditContainer, heights);
} }
}; };
/**
* Restores the height of each textarea in the container
* @param container The container to search for textareas
* @param heights An object with textarea ids as keys and heights as values
*/
PromptManagerModule.prototype.restoreTextAreaHeights = function(container, heights) {
if (Object.keys(heights).length === 0) return;
$(container).find('textarea').each(function () {
const height = heights[this.id];
if (height > 0) $(this).height(height);
});
}
/**
* Saves the current height of each textarea in the container
* @param container The container to search for textareas
* @returns {{}} An object with textarea ids as keys and heights as values
*/
PromptManagerModule.prototype.saveTextAreaHeights = function(container) {
const heights = {};
$(container).find('textarea').each(function () {
heights[this.id] = $(this).height();
});
return heights;
}
/** /**
* Empties, then re-assembles the prompt list * Empties, then re-assembles the prompt list
*/ */

View File

@ -2,15 +2,12 @@ esversion: 6
import { import {
Generate, Generate,
this_chid,
characters, characters,
online_status, online_status,
main_api, main_api,
api_server, api_server,
api_server_textgenerationwebui, api_server_textgenerationwebui,
is_send_press, is_send_press,
getTokenCount,
menu_type,
max_context, max_context,
saveSettingsDebounced, saveSettingsDebounced,
active_group, active_group,
@ -33,10 +30,9 @@ import {
SECRET_KEYS, SECRET_KEYS,
secret_state, secret_state,
} from "./secrets.js"; } from "./secrets.js";
import { debounce, delay, getStringHash } from "./utils.js"; import { debounce, delay, getStringHash, 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";
var NavToggle = document.getElementById("nav-toggle");
var RPanelPin = document.getElementById("rm_button_panel_pin"); var RPanelPin = document.getElementById("rm_button_panel_pin");
var LPanelPin = document.getElementById("lm_button_panel_pin"); var LPanelPin = document.getElementById("lm_button_panel_pin");
@ -47,20 +43,8 @@ var LeftNavPanel = document.getElementById("left-nav-panel");
var WorldInfo = document.getElementById("WorldInfo"); var WorldInfo = document.getElementById("WorldInfo");
var SelectedCharacterTab = document.getElementById("rm_button_selected_ch"); var SelectedCharacterTab = document.getElementById("rm_button_selected_ch");
var AdvancedCharDefsPopup = document.getElementById("character_popup");
var ConfirmationPopup = document.getElementById("dialogue_popup");
var AutoConnectCheckbox = document.getElementById("auto-connect-checkbox"); var AutoConnectCheckbox = document.getElementById("auto-connect-checkbox");
var AutoLoadChatCheckbox = document.getElementById("auto-load-chat-checkbox"); var AutoLoadChatCheckbox = document.getElementById("auto-load-chat-checkbox");
var SelectedNavTab = ("#" + LoadLocal('SelectedNavTab'));
var create_save_name;
var create_save_description;
var create_save_personality;
var create_save_first_message;
var create_save_scenario;
var create_save_mes_example;
var count_tokens;
var perm_tokens;
var connection_made = false; var connection_made = false;
var retry_delay = 500; var retry_delay = 500;
@ -83,32 +67,6 @@ const observer = new MutationObserver(function (mutations) {
observer.observe(document.documentElement, observerConfig); observer.observe(document.documentElement, observerConfig);
/**
* Wait for an element before resolving a promise
* @param {String} querySelector - Selector of element to wait for
* @param {Integer} timeout - Milliseconds to wait before timing out, or 0 for no timeout
*/
function waitForElement(querySelector, timeout) {
return new Promise((resolve, reject) => {
var timer = false;
if (document.querySelectorAll(querySelector).length) return resolve();
const observer = new MutationObserver(() => {
if (document.querySelectorAll(querySelector).length) {
observer.disconnect();
if (timer !== false) clearTimeout(timer);
return resolve();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
if (timeout) timer = setTimeout(() => {
observer.disconnect();
reject();
}, timeout);
});
}
/** /**
* Converts generation time from milliseconds to a human-readable format. * Converts generation time from milliseconds to a human-readable format.
@ -225,14 +183,6 @@ export function getMessageTimeStamp() {
// triggers: // triggers:
$("#rm_button_create").on("click", function () { //when "+New Character" is clicked $("#rm_button_create").on("click", function () { //when "+New Character" is clicked
$(SelectedCharacterTab).children("h2").html(''); // empty nav's 3rd panel tab $(SelectedCharacterTab).children("h2").html(''); // empty nav's 3rd panel tab
//empty temp vars to store new char data for counting
create_save_name = "";
create_save_description = "";
create_save_personality = "";
create_save_first_message = "";
create_save_scenario = "";
create_save_mes_example = "";
}); });
//when any input is made to the create/edit character form textareas //when any input is made to the create/edit character form textareas
$("#rm_ch_create_block").on("input", function () { countTokensDebounced(); }); $("#rm_ch_create_block").on("input", function () { countTokensDebounced(); });
@ -245,7 +195,7 @@ export function RA_CountCharTokens() {
$('[data-token-counter]').each(function () { $('[data-token-counter]').each(function () {
const counter = $(this); const counter = $(this);
const input = $(document.getElementById(counter.data('token-counter'))); const input = $(document.getElementById(counter.data('token-counter')));
const value = input.val(); const value = String(input.val());
if (input.length === 0) { if (input.length === 0) {
counter.text('Invalid input reference'); counter.text('Invalid input reference');
@ -413,7 +363,7 @@ function RA_autoconnect(PrevApi) {
case 'openai': case 'openai':
if (((secret_state[SECRET_KEYS.OPENAI] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.OPENAI) if (((secret_state[SECRET_KEYS.OPENAI] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.OPENAI)
|| ((secret_state[SECRET_KEYS.CLAUDE] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) || ((secret_state[SECRET_KEYS.CLAUDE] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.CLAUDE)
|| (secret_state[SECRET_KEYS.SCALE] && oai_settings.chat_completion_source == chat_completion_sources.SCALE) || ((secret_state[SECRET_KEYS.SCALE] || secret_state[SECRET_KEYS.SCALE_COOKIE]) && oai_settings.chat_completion_source == chat_completion_sources.SCALE)
|| (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) || (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI)
|| (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)
|| (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21) || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
@ -717,7 +667,12 @@ export async function initMovingUI() {
// --------------------------------------------------- // ---------------------------------------------------
$("document").ready(function () { jQuery(async function () {
try {
await waitUntilCondition(() => online_status !== undefined, 1000, 10);
} catch {
console.log('Timeout waiting for online_status');
}
// initial status check // initial status check
setTimeout(() => { setTimeout(() => {
@ -799,7 +754,7 @@ $("document").ready(function () {
//console.log('setting pin class via local var'); //console.log('setting pin class via local var');
$(RightNavPanel).addClass('pinnedOpen'); $(RightNavPanel).addClass('pinnedOpen');
} }
if ($(RPanelPin).prop('checked' == true)) { if (!!$(RPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state'); console.debug('setting pin class via checkbox state');
$(RightNavPanel).addClass('pinnedOpen'); $(RightNavPanel).addClass('pinnedOpen');
} }
@ -809,7 +764,7 @@ $("document").ready(function () {
//console.log('setting pin class via local var'); //console.log('setting pin class via local var');
$(LeftNavPanel).addClass('pinnedOpen'); $(LeftNavPanel).addClass('pinnedOpen');
} }
if ($(LPanelPin).prop('checked' == true)) { if (!!$(LPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state'); console.debug('setting pin class via checkbox state');
$(LeftNavPanel).addClass('pinnedOpen'); $(LeftNavPanel).addClass('pinnedOpen');
} }
@ -821,7 +776,7 @@ $("document").ready(function () {
$(WorldInfo).addClass('pinnedOpen'); $(WorldInfo).addClass('pinnedOpen');
} }
if ($(WIPanelPin).prop('checked' == true)) { if (!!$(WIPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state'); console.debug('setting pin class via checkbox state');
$(WorldInfo).addClass('pinnedOpen'); $(WorldInfo).addClass('pinnedOpen');
} }
@ -884,8 +839,6 @@ $("document").ready(function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
//this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height) //this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
$('#send_textarea').on('input', function () { $('#send_textarea').on('input', function () {
this.style.height = '40px'; this.style.height = '40px';
@ -896,7 +849,7 @@ $("document").ready(function () {
document.addEventListener('swiped-left', function (e) { document.addEventListener('swiped-left', function (e) {
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) {
if (SwipeButR.css('display') === 'flex') { if (SwipeButR.css('display') === 'flex') {
SwipeButR.click(); SwipeButR.click();
@ -905,7 +858,7 @@ $("document").ready(function () {
}); });
document.addEventListener('swiped-right', function (e) { document.addEventListener('swiped-right', function (e) {
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) {
if (SwipeButL.css('display') === 'flex') { if (SwipeButL.css('display') === 'flex') {
SwipeButL.click(); SwipeButL.click();

View File

@ -2,7 +2,6 @@ import {
chat_metadata, chat_metadata,
eventSource, eventSource,
event_types, event_types,
getTokenCount,
saveSettingsDebounced, saveSettingsDebounced,
this_chid, this_chid,
} from "../script.js"; } from "../script.js";
@ -10,6 +9,7 @@ import { selected_group } from "./group-chats.js";
import { extension_settings, getContext, saveMetadataDebounced } from "./extensions.js"; import { extension_settings, getContext, saveMetadataDebounced } from "./extensions.js";
import { registerSlashCommand } from "./slash-commands.js"; import { registerSlashCommand } from "./slash-commands.js";
import { getCharaFilename, debounce, waitUntilCondition, delay } from "./utils.js"; import { getCharaFilename, debounce, waitUntilCondition, delay } from "./utils.js";
import { getTokenCount } from "./tokenizers.js";
export { MODULE_NAME as NOTE_MODULE_NAME }; export { MODULE_NAME as NOTE_MODULE_NAME };
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory

View File

@ -1,5 +1,5 @@
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders } from "../script.js"; import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams } from "../script.js";
import { isSubsetOf, debounce } from "./utils.js"; import { isSubsetOf, debounce, waitUntilCondition } from "./utils.js";
export { export {
getContext, getContext,
getApiUrl, getApiUrl,
@ -12,10 +12,46 @@ export {
}; };
let extensionNames = []; let extensionNames = [];
let manifests = []; let manifests = {};
const defaultUrl = "http://localhost:5100"; const defaultUrl = "http://localhost:5100";
export const saveMetadataDebounced = debounce(async () => await getContext().saveMetadata(), 1000); export const saveMetadataDebounced = debounce(async () => await getContext().saveMetadata(), 1000);
export const extensionsHandlebars = Handlebars.create();
/**
* Registers a Handlebars helper for use in extensions.
* @param {string} name Handlebars helper name
* @param {function} helper Handlebars helper function
*/
export function registerExtensionHelper(name, helper) {
extensionsHandlebars.registerHelper(name, helper);
}
/**
* Applies handlebars extension helpers to a message.
* @param {number} messageId Message index in the chat.
*/
export function processExtensionHelpers(messageId) {
const context = getContext();
const message = context.chat[messageId];
if (!message?.mes || typeof message.mes !== 'string') {
return;
}
// Don't waste time if there are no mustaches
if (!substituteParams(message.mes).includes('{{')) {
return;
}
try {
const template = extensionsHandlebars.compile(substituteParams(message.mes), { noEscape: true });
message.mes = template({});
} catch {
// Ignore
}
}
// Disables parallel updates // Disables parallel updates
class ModuleWorkerWrapper { class ModuleWorkerWrapper {
constructor(callback) { constructor(callback) {
@ -175,7 +211,10 @@ async function getManifests(names) {
} else { } else {
reject(); reject();
} }
}).catch(err => reject() && console.log('Could not load manifest.json for ' + name, err)); }).catch(err => {
reject();
console.log('Could not load manifest.json for ' + name, err);
});
}); });
promises.push(promise); promises.push(promise);
@ -232,9 +271,9 @@ async function activateExtensions() {
async function connectClickHandler() { async function connectClickHandler() {
const baseUrl = $("#extensions_url").val(); const baseUrl = $("#extensions_url").val();
extension_settings.apiUrl = baseUrl; extension_settings.apiUrl = String(baseUrl);
const testApiKey = $("#extensions_api_key").val(); const testApiKey = $("#extensions_api_key").val();
extension_settings.apiKey = testApiKey; extension_settings.apiKey = String(testApiKey);
saveSettingsDebounced(); saveSettingsDebounced();
await connectToApi(baseUrl); await connectToApi(baseUrl);
} }
@ -459,7 +498,7 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt
* Gets extension data and generates the corresponding HTML for displaying the extension. * Gets extension data and generates the corresponding HTML for displaying the extension.
* *
* @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest. * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest.
* @return {object} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. * @return {Promise<object>} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string.
*/ */
async function getExtensionData(extension) { async function getExtensionData(extension) {
const name = extension[0]; const name = extension[0];
@ -576,7 +615,7 @@ async function onDeleteClick() {
* Fetches the version details of a specific extension. * Fetches the version details of a specific extension.
* *
* @param {string} extensionName - The name of the extension. * @param {string} extensionName - The name of the extension.
* @return {object} - An object containing the extension's version details. * @return {Promise<object>} - An object containing the extension's version details.
* This object includes the currentBranchName, currentCommitHash, isUpToDate, and remoteUrl. * This object includes the currentBranchName, currentCommitHash, isUpToDate, and remoteUrl.
* @throws {error} - If there is an error during the fetch operation, it logs the error to the console. * @throws {error} - If there is an error during the fetch operation, it logs the error to the console.
*/ */
@ -629,8 +668,8 @@ async function runGenerationInterceptors(chat, contextSize) {
} }
} }
$(document).ready(async function () { jQuery(function () {
setTimeout(function () { setTimeout(async function () {
addExtensionsButtonAndMenu(); addExtensionsButtonAndMenu();
$("#extensionsMenuButton").css("display", "flex"); $("#extensionsMenuButton").css("display", "flex");
}, 100) }, 100)

View File

@ -1,6 +1,7 @@
import { getBase64Async } from "../../utils.js"; import { getBase64Async } from "../../utils.js";
import { getContext, getApiUrl, doExtrasFetch, extension_settings } from "../../extensions.js"; import { getContext, getApiUrl, doExtrasFetch, extension_settings } from "../../extensions.js";
import { callPopup, saveSettingsDebounced } from "../../../script.js"; import { callPopup, saveSettingsDebounced } from "../../../script.js";
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'caption'; const MODULE_NAME = 'caption';
@ -52,7 +53,7 @@ async function sendCaptionedMessage(caption, image) {
name: context.name1, name: context.name1,
is_user: true, is_user: true,
is_name: true, is_name: true,
send_date: Date.now(), send_date: getMessageTimeStamp(),
mes: messageText, mes: messageText,
extra: { extra: {
image: image, image: image,

View File

@ -23,7 +23,8 @@ const defaultSettings = {
}; };
const settingType = { const settingType = {
guidance_scale: 0, guidance_scale: 0,
negative_prompt: 1 negative_prompt: 1,
positive_prompt: 2
} }
// Used for character and chat CFG values // Used for character and chat CFG values
@ -36,19 +37,19 @@ function setCharCfg(tempValue, setting) {
const avatarName = getCharaFilename(); const avatarName = getCharaFilename();
// Assign temp object // Assign temp object
let tempCharaCfg; let tempCharaCfg = {
name: avatarName
};
switch(setting) { switch(setting) {
case settingType.guidance_scale: case settingType.guidance_scale:
tempCharaCfg = { tempCharaCfg["guidance_scale"] = Number(tempValue);
"name": avatarName,
"guidance_scale": Number(tempValue)
}
break; break;
case settingType.negative_prompt: case settingType.negative_prompt:
tempCharaCfg = { tempCharaCfg["negative_prompt"] = tempValue;
"name": avatarName, break;
"negative_prompt": tempValue case settingType.positive_prompt:
} tempCharaCfg["positive_prompt"] = tempValue;
break; break;
default: default:
return false; return false;
@ -66,7 +67,11 @@ function setCharCfg(tempValue, setting) {
const tempAssign = Object.assign(existingCharaCfg, tempCharaCfg); const tempAssign = Object.assign(existingCharaCfg, tempCharaCfg);
// If both values are default, remove the entry // If both values are default, remove the entry
if (!existingCharaCfg.useChara && (tempAssign.guidance_scale ?? 1.00) === 1.00 && (tempAssign.negative_prompt?.length ?? 0) === 0) { if (!existingCharaCfg.useChara &&
(tempAssign.guidance_scale ?? 1.00) === 1.00 &&
(tempAssign.negative_prompt?.length ?? 0) === 0 &&
(tempAssign.positive_prompt?.length ?? 0) === 0)
{
extension_settings.cfg.chara.splice(existingCharaCfgIndex, 1); extension_settings.cfg.chara.splice(existingCharaCfgIndex, 1);
} }
} else if (avatarName && tempValue.length > 0) { } else if (avatarName && tempValue.length > 0) {
@ -95,6 +100,9 @@ function setChatCfg(tempValue, setting) {
case settingType.negative_prompt: case settingType.negative_prompt:
chat_metadata[metadataKeys.negative_prompt] = tempValue; chat_metadata[metadataKeys.negative_prompt] = tempValue;
break; break;
case settingType.positive_prompt:
chat_metadata[metadataKeys.positive_prompt] = tempValue;
break;
default: default:
return false; return false;
} }
@ -174,20 +182,40 @@ function loadSettings() {
$('#chat_cfg_guidance_scale').val(chat_metadata[metadataKeys.guidance_scale] ?? 1.0.toFixed(2)); $('#chat_cfg_guidance_scale').val(chat_metadata[metadataKeys.guidance_scale] ?? 1.0.toFixed(2));
$('#chat_cfg_guidance_scale_counter').text(chat_metadata[metadataKeys.guidance_scale]?.toFixed(2) ?? 1.0.toFixed(2)); $('#chat_cfg_guidance_scale_counter').text(chat_metadata[metadataKeys.guidance_scale]?.toFixed(2) ?? 1.0.toFixed(2));
$('#chat_cfg_negative_prompt').val(chat_metadata[metadataKeys.negative_prompt] ?? ''); $('#chat_cfg_negative_prompt').val(chat_metadata[metadataKeys.negative_prompt] ?? '');
$('#chat_cfg_positive_prompt').val(chat_metadata[metadataKeys.positive_prompt] ?? '');
$('#groupchat_cfg_use_chara').prop('checked', chat_metadata[metadataKeys.groupchat_individual_chars] ?? false); $('#groupchat_cfg_use_chara').prop('checked', chat_metadata[metadataKeys.groupchat_individual_chars] ?? false);
if (chat_metadata[metadataKeys.negative_combine]?.length > 0) { if (chat_metadata[metadataKeys.prompt_combine]?.length > 0) {
chat_metadata[metadataKeys.negative_combine].forEach((element) => { chat_metadata[metadataKeys.prompt_combine].forEach((element) => {
$(`input[name="cfg_negative_combine"][value="${element}"]`) $(`input[name="cfg_prompt_combine"][value="${element}"]`)
.prop("checked", true); .prop("checked", true);
}); });
} }
// Display the negative separator in quotes if not quoted already
let promptSeparatorDisplay = [];
const promptSeparator = chat_metadata[metadataKeys.prompt_separator];
if (promptSeparator) {
promptSeparatorDisplay.push(promptSeparator);
if (!promptSeparator.startsWith(`"`)) {
promptSeparatorDisplay.unshift(`"`);
}
if (!promptSeparator.endsWith(`"`)) {
promptSeparatorDisplay.push(`"`);
}
}
$('#cfg_prompt_separator').val(promptSeparatorDisplay.length === 0 ? '' : promptSeparatorDisplay.join(''));
$('#cfg_prompt_insertion_depth').val(chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1);
// Set character CFG if it exists // Set character CFG if it exists
if (!selected_group) { if (!selected_group) {
const charaCfg = extension_settings.cfg.chara.find((e) => e.name === getCharaFilename()); const charaCfg = extension_settings.cfg.chara.find((e) => e.name === getCharaFilename());
$('#chara_cfg_guidance_scale').val(charaCfg?.guidance_scale ?? 1.00); $('#chara_cfg_guidance_scale').val(charaCfg?.guidance_scale ?? 1.00);
$('#chara_cfg_guidance_scale_counter').text(charaCfg?.guidance_scale?.toFixed(2) ?? 1.0.toFixed(2)); $('#chara_cfg_guidance_scale_counter').text(charaCfg?.guidance_scale?.toFixed(2) ?? 1.0.toFixed(2));
$('#chara_cfg_negative_prompt').val(charaCfg?.negative_prompt ?? ''); $('#chara_cfg_negative_prompt').val(charaCfg?.negative_prompt ?? '');
$('#chara_cfg_positive_prompt').val(charaCfg?.positive_prompt ?? '');
} }
} }
@ -204,26 +232,50 @@ async function initialLoadSettings() {
$('#global_cfg_guidance_scale').val(extension_settings.cfg.global.guidance_scale); $('#global_cfg_guidance_scale').val(extension_settings.cfg.global.guidance_scale);
$('#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));
$('#global_cfg_negative_prompt').val(extension_settings.cfg.global.negative_prompt); $('#global_cfg_negative_prompt').val(extension_settings.cfg.global.negative_prompt);
$('#global_cfg_positive_prompt').val(extension_settings.cfg.global.positive_prompt);
} }
function migrateSettings() { function migrateSettings() {
let performSave = false; let performSettingsSave = false;
let performMetaSave = false;
if (power_user.guidance_scale) { if (power_user.guidance_scale) {
extension_settings.cfg.global.guidance_scale = power_user.guidance_scale; extension_settings.cfg.global.guidance_scale = power_user.guidance_scale;
delete power_user['guidance_scale']; delete power_user['guidance_scale'];
performSave = true; performSettingsSave = true;
} }
if (power_user.negative_prompt) { if (power_user.negative_prompt) {
extension_settings.cfg.global.negative_prompt = power_user.negative_prompt; extension_settings.cfg.global.negative_prompt = power_user.negative_prompt;
delete power_user['negative_prompt']; delete power_user['negative_prompt'];
performSave = true; performSettingsSave = true;
} }
if (performSave) { if (chat_metadata["cfg_negative_combine"]) {
chat_metadata[metadataKeys.prompt_combine] = chat_metadata["cfg_negative_combine"];
chat_metadata["cfg_negative_combine"] = undefined;
performMetaSave = true;
}
if (chat_metadata["cfg_negative_insertion_depth"]) {
chat_metadata[metadataKeys.prompt_insertion_depth] = chat_metadata["cfg_negative_insertion_depth"];
chat_metadata["cfg_negative_insertion_depth"] = undefined;
performMetaSave = true;
}
if (chat_metadata["cfg_negative_separator"]) {
chat_metadata[metadataKeys.prompt_separator] = chat_metadata["cfg_negative_separator"];
chat_metadata["cfg_negative_separator"] = undefined;
performMetaSave = true;
}
if (performSettingsSave) {
saveSettingsDebounced(); saveSettingsDebounced();
} }
if (performMetaSave) {
saveMetadataDebounced();
}
} }
// This function is called when the extension is loaded // This function is called when the extension is loaded
@ -255,6 +307,10 @@ jQuery(async () => {
setChatCfg($(this).val(), settingType.negative_prompt); setChatCfg($(this).val(), settingType.negative_prompt);
}); });
windowHtml.find('#chat_cfg_positive_prompt').on('input', function() {
setChatCfg($(this).val(), settingType.positive_prompt);
});
windowHtml.find('#chara_cfg_guidance_scale').on('input', function() { windowHtml.find('#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);
@ -267,6 +323,10 @@ jQuery(async () => {
setCharCfg($(this).val(), settingType.negative_prompt); setCharCfg($(this).val(), settingType.negative_prompt);
}); });
windowHtml.find('#chara_cfg_positive_prompt').on('input', function() {
setCharCfg($(this).val(), settingType.positive_prompt);
});
windowHtml.find('#global_cfg_guidance_scale').on('input', function() { windowHtml.find('#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));
@ -278,14 +338,29 @@ jQuery(async () => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
windowHtml.find(`input[name="cfg_negative_combine"]`).on('input', function() { windowHtml.find('#global_cfg_positive_prompt').on('input', function() {
const values = windowHtml.find(`input[name="cfg_negative_combine"]`) extension_settings.cfg.global.positive_prompt = $(this).val();
saveSettingsDebounced();
});
windowHtml.find(`input[name="cfg_prompt_combine"]`).on('input', function() {
const values = windowHtml.find(`input[name="cfg_prompt_combine"]`)
.filter(":checked") .filter(":checked")
.map(function() { return parseInt($(this).val()) }) .map(function() { return parseInt($(this).val()) })
.get() .get()
.filter((e) => e !== NaN) || []; .filter((e) => e !== NaN) || [];
chat_metadata[metadataKeys.negative_combine] = values; chat_metadata[metadataKeys.prompt_combine] = values;
saveMetadataDebounced();
});
windowHtml.find(`#cfg_prompt_insertion_depth`).on('input', function() {
chat_metadata[metadataKeys.prompt_insertion_depth] = Number($(this).val());
saveMetadataDebounced();
});
windowHtml.find(`#cfg_prompt_separator`).on('input', function() {
chat_metadata[metadataKeys.prompt_separator] = $(this).val();
saveMetadataDebounced(); saveMetadataDebounced();
}); });

View File

@ -1,4 +1,4 @@
import { chat_metadata, this_chid } from "../../../script.js"; import { chat_metadata, substituteParams, this_chid } from "../../../script.js";
import { extension_settings, getContext } from "../../extensions.js" import { extension_settings, getContext } from "../../extensions.js"
import { selected_group } from "../../group-chats.js"; import { selected_group } from "../../group-chats.js";
import { getCharaFilename } from "../../utils.js"; import { getCharaFilename } from "../../utils.js";
@ -11,46 +11,20 @@ export const cfgType = {
export const metadataKeys = { export const metadataKeys = {
guidance_scale: "cfg_guidance_scale", guidance_scale: "cfg_guidance_scale",
negative_prompt: "cfg_negative_prompt", negative_prompt: "cfg_negative_prompt",
negative_combine: "cfg_negative_combine", positive_prompt: "cfg_positive_prompt",
groupchat_individual_chars: "cfg_groupchat_individual_chars" 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 value from hierarchy of chat -> character -> global // Gets the CFG guidance scale
// Returns undefined values which should be handled in the respective backend APIs // If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
export function getCfg() { export function getGuidanceScale() {
let splitNegativePrompt = [];
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid)); const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
const guidanceScale = getGuidanceScale(charaCfg);
const chatNegativeCombine = chat_metadata[metadataKeys.negative_combine] ?? [];
// If there's a guidance scale, continue. Otherwise assume undefined
if (guidanceScale?.value && guidanceScale?.value !== 1) {
if (guidanceScale.type === cfgType.chat || chatNegativeCombine.includes(cfgType.chat)) {
splitNegativePrompt.push(chat_metadata[metadataKeys.negative_prompt]?.trim());
}
if (guidanceScale.type === cfgType.chara || chatNegativeCombine.includes(cfgType.chara)) {
splitNegativePrompt.push(charaCfg.negative_prompt?.trim())
}
if (guidanceScale.type === cfgType.global || chatNegativeCombine.includes(cfgType.global)) {
splitNegativePrompt.push(extension_settings.cfg.global.negative_prompt?.trim());
}
const combinedNegatives = splitNegativePrompt.filter((e) => e.length > 0).join(", ");
console.debug(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedNegatives}`)
return {
guidanceScale: guidanceScale.value,
negativePrompt: combinedNegatives
}
}
}
// If the guidance scale is 1, ignore the CFG negative prompt since it won't be used anyways
function getGuidanceScale(charaCfg) {
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale]; const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false; const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) { if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) {
return { return {
type: cfgType.chat, type: cfgType.chat,
@ -70,3 +44,48 @@ function getGuidanceScale(charaCfg) {
value: extension_settings.cfg.global.guidance_scale 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]
)
?.trim()
);
}
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
)
?.trim()
);
}
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
)
?.trim()
);
}
// 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

@ -14,7 +14,7 @@
<small> <small>
<b>Unique to this chat.</b><br> <b>Unique to this chat.</b><br>
</small> </small>
<label for="chat_cfg_negative_prompt"> <label for="chat_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span> <span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small> <small data-i18n="1 = disabled">1 = disabled</small>
</label> </label>
@ -33,6 +33,11 @@
<span data-i18n="Negative Prompt">Negative Prompt</span> <span data-i18n="Negative Prompt">Negative Prompt</span>
</label> </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> <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>
<div id="groupchat_cfg_use_chara_container"> <div id="groupchat_cfg_use_chara_container">
<label class="checkbox_label" for="groupchat_cfg_use_chara"> <label class="checkbox_label" for="groupchat_cfg_use_chara">
@ -53,7 +58,7 @@
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<small><b>Will be automatically added as the CFG for this character.</b></small> <small><b>Will be automatically added as the CFG for this character.</b></small>
<br /> <br />
<label for="chara_cfg_negative_prompt"> <label for="chara_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span> <span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small> <small data-i18n="1 = disabled">1 = disabled</small>
</label> </label>
@ -72,6 +77,11 @@
<span data-i18n="Negative Prompt">Negative Prompt</span> <span data-i18n="Negative Prompt">Negative Prompt</span>
</label> </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> <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> </div>
@ -86,7 +96,7 @@
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<small><b>Will be used as the default CFG options for every chat unless overridden.</b></small> <small><b>Will be used as the default CFG options for every chat unless overridden.</b></small>
<br /> <br />
<label for="global_cfg_negative_prompt"> <label for="global_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span> <span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small> <small data-i18n="1 = disabled">1 = disabled</small>
</label> </label>
@ -105,40 +115,57 @@
<span data-i18n="Negative Prompt">Negative Prompt</span> <span data-i18n="Negative Prompt">Negative Prompt</span>
</label> </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> <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> </div>
</div> </div>
<div id="cfg_negative_combine_container"> <div id="cfg_prompt_combine_container">
<hr class="sysHR"> <hr class="sysHR">
<div class="inline-drawer"> <div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header"> <div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Negative Cascading</b> <b>CFG Prompt Cascading</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">
<div class="flex-container flexFlowColumn">
<small> <small>
<b>Combine negative prompts from other boxes.</b> <b>Combine positive/negative prompts from other boxes.</b>
<br /> <br />
For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string. For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.
</small> </small>
</div>
<br /> <br />
<label for="cfg_negative_combine"> <div class="flex-container flexFlowColumn">
<label for="cfg_prompt_combine">
<span data-i18n="Scale">Always Include</span> <span data-i18n="Scale">Always Include</span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" name="cfg_negative_combine" value="0" /> <input type="checkbox" name="cfg_prompt_combine" value="0" />
<span data-i18n="Chat Negatives">Chat Negatives</span> <span data-i18n="Chat Negatives">Chat Negatives</span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" name="cfg_negative_combine" value="1" /> <input type="checkbox" name="cfg_prompt_combine" value="1" />
<span data-i18n="Character Negatives">Character Negatives</span> <span data-i18n="Character Negatives">Character Negatives</span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" name="cfg_negative_combine" value="2" /> <input type="checkbox" name="cfg_prompt_combine" value="2" />
<span data-i18n="Global Negatives">Global Negatives</span> <span data-i18n="Global Negatives">Global Negatives</span>
</label> </label>
</div> </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>
</div> </div>

View File

@ -1,6 +1,7 @@
import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, CHARACTERS_PER_TOKEN_RATIO, substituteParams, max_context, } from "../../../script.js"; import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, substituteParams, } from "../../../script.js";
import { humanizedDateTime } from "../../RossAscends-mods.js"; import { humanizedDateTime } from "../../RossAscends-mods.js";
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.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"; import { getFileText, onlyUnique, splitRecursive } from "../../utils.js";
export { MODULE_NAME }; export { MODULE_NAME };

View File

@ -28,7 +28,7 @@ async function updateQuickReplyPresetList() {
if (result.ok) { if (result.ok) {
var data = await result.json(); var data = await result.json();
presets = data.quickReplyPresets?.length ? data.quickReplyPresets : []; presets = data.quickReplyPresets?.length ? data.quickReplyPresets : [];
console.log(presets) console.debug('Quick Reply presets', presets);
$("#quickReplyPresets").find('option[value!=""]').remove(); $("#quickReplyPresets").find('option[value!=""]').remove();

View File

@ -4,11 +4,12 @@ TODO:
*/ */
import { saveSettingsDebounced } from "../../../script.js"; import { saveSettingsDebounced } from "../../../script.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js"; import { getContext, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
import { VoskSttProvider } from './vosk.js' import { VoskSttProvider } from './vosk.js'
import { WhisperSttProvider } from './whisper.js' import { WhisperSttProvider } from './whisper.js'
import { BrowserSttProvider } from './browser.js' import { BrowserSttProvider } from './browser.js'
import { StreamingSttProvider } from './streaming.js' import { StreamingSttProvider } from './streaming.js'
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'Speech Recognition'; const MODULE_NAME = 'Speech Recognition';
@ -152,7 +153,7 @@ async function processTranscript(transcript) {
name: context.name1, name: context.name1,
is_user: true, is_user: true,
is_name: true, is_name: true,
send_date: Date.now(), send_date: getMessageTimeStamp(),
mes: messageText, mes: messageText,
}; };
context.chat.push(message); context.chat.push(message);

View File

@ -14,7 +14,7 @@ import {
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js"; import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
import { selected_group } from "../../group-chats.js"; import { selected_group } from "../../group-chats.js";
import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename, saveBase64AsFile } from "../../utils.js"; import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename, saveBase64AsFile } from "../../utils.js";
import { humanizedDateTime } from "../../RossAscends-mods.js"; import { getMessageTimeStamp, humanizedDateTime } from "../../RossAscends-mods.js";
export { MODULE_NAME }; export { MODULE_NAME };
// Wraps a string into monospace font-face span // Wraps a string into monospace font-face span
@ -755,11 +755,10 @@ async function sendMessage(prompt, image) {
const messageText = `[${context.name2} sends a picture that contains: ${prompt}]`; const messageText = `[${context.name2} sends a picture that contains: ${prompt}]`;
const message = { const message = {
name: context.groupId ? systemUserName : context.name2, name: context.groupId ? systemUserName : context.name2,
is_system: context.groupId ? true : false,
is_user: false, is_user: false,
is_system: true, is_system: true,
is_name: true, is_name: true,
send_date: timestampToMoment(Date.now()).format('LL LT'), send_date: getMessageTimeStamp(),
mes: context.groupId ? p(messageText) : messageText, mes: context.groupId ? p(messageText) : messageText,
extra: { extra: {
image: image, image: image,

View File

@ -1,6 +1,6 @@
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 "../../openai.js"; import { getTokenizerModel } from "../../tokenizers.js";
async function doTokenCounter() { async function doTokenCounter() {
const selectedTokenizer = main_api == 'openai' const selectedTokenizer = main_api == 'openai'

View File

@ -421,9 +421,9 @@ jQuery(() => {
loadSettings(); loadSettings();
eventSource.on(event_types.MESSAGE_RECEIVED, handleIncomingMessage); eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage); eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
eventSource.on(event_types.MESSAGE_SENT, handleOutgoingMessage); eventSource.on(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady); eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit); eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit);

View File

@ -0,0 +1,66 @@
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

@ -0,0 +1,11 @@
{
"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

@ -1,6 +1,10 @@
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchWorldInfo, power_user } from "./power-user.js"; import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchWorldInfo, power_user } from "./power-user.js";
import { tag_map } from "./tags.js"; import { tag_map } from "./tags.js";
/**
* The filter types.
* @type {Object.<string, string>}
*/
export const FILTER_TYPES = { export const FILTER_TYPES = {
SEARCH: 'search', SEARCH: 'search',
TAG: 'tag', TAG: 'tag',
@ -9,11 +13,26 @@ export const FILTER_TYPES = {
WORLD_INFO_SEARCH: 'world_info_search', WORLD_INFO_SEARCH: 'world_info_search',
}; };
/**
* Helper class for filtering data.
* @example
* const filterHelper = new FilterHelper(() => console.log('data changed'));
* filterHelper.setFilterData(FILTER_TYPES.SEARCH, 'test');
* data = filterHelper.applyFilters(data);
*/
export class FilterHelper { export class FilterHelper {
/**
* Creates a new FilterHelper
* @param {Function} onDataChanged Callback to trigger when the filter data changes
*/
constructor(onDataChanged) { constructor(onDataChanged) {
this.onDataChanged = onDataChanged; this.onDataChanged = onDataChanged;
} }
/**
* The filter functions.
* @type {Object.<string, Function>}
*/
filterFunctions = { filterFunctions = {
[FILTER_TYPES.SEARCH]: this.searchFilter.bind(this), [FILTER_TYPES.SEARCH]: this.searchFilter.bind(this),
[FILTER_TYPES.GROUP]: this.groupFilter.bind(this), [FILTER_TYPES.GROUP]: this.groupFilter.bind(this),
@ -22,6 +41,10 @@ export class FilterHelper {
[FILTER_TYPES.WORLD_INFO_SEARCH]: this.wiSearchFilter.bind(this), [FILTER_TYPES.WORLD_INFO_SEARCH]: this.wiSearchFilter.bind(this),
} }
/**
* The filter data.
* @type {Object.<string, any>}
*/
filterData = { filterData = {
[FILTER_TYPES.SEARCH]: '', [FILTER_TYPES.SEARCH]: '',
[FILTER_TYPES.GROUP]: false, [FILTER_TYPES.GROUP]: false,
@ -30,6 +53,11 @@ export class FilterHelper {
[FILTER_TYPES.WORLD_INFO_SEARCH]: '', [FILTER_TYPES.WORLD_INFO_SEARCH]: '',
} }
/**
* Applies a fuzzy search filter to the World Info data.
* @param {any[]} data The data to filter. Must have a uid property.
* @returns {any[]} The filtered data.
*/
wiSearchFilter(data) { wiSearchFilter(data) {
const term = this.filterData[FILTER_TYPES.WORLD_INFO_SEARCH]; const term = this.filterData[FILTER_TYPES.WORLD_INFO_SEARCH];
@ -41,6 +69,11 @@ export class FilterHelper {
return data.filter(entity => fuzzySearchResults.includes(entity.uid)); return data.filter(entity => fuzzySearchResults.includes(entity.uid));
} }
/**
* Applies a tag filter to the data.
* @param {any[]} data The data to filter.
* @returns {any[]} The filtered data.
*/
tagFilter(data) { tagFilter(data) {
const TAG_LOGIC_AND = true; // switch to false to use OR logic for combining tags const TAG_LOGIC_AND = true; // switch to false to use OR logic for combining tags
const { selected, excluded } = this.filterData[FILTER_TYPES.TAG]; const { selected, excluded } = this.filterData[FILTER_TYPES.TAG];
@ -76,6 +109,11 @@ export class FilterHelper {
return data.filter(entity => getIsTagged(entity)); return data.filter(entity => getIsTagged(entity));
} }
/**
* Applies a favorite filter to the data.
* @param {any[]} data The data to filter.
* @returns {any[]} The filtered data.
*/
favFilter(data) { favFilter(data) {
if (!this.filterData[FILTER_TYPES.FAV]) { if (!this.filterData[FILTER_TYPES.FAV]) {
return data; return data;
@ -84,6 +122,11 @@ export class FilterHelper {
return data.filter(entity => entity.item.fav || entity.item.fav == "true"); return data.filter(entity => entity.item.fav || entity.item.fav == "true");
} }
/**
* Applies a group type filter to the data.
* @param {any[]} data The data to filter.
* @returns {any[]} The filtered data.
*/
groupFilter(data) { groupFilter(data) {
if (!this.filterData[FILTER_TYPES.GROUP]) { if (!this.filterData[FILTER_TYPES.GROUP]) {
return data; return data;
@ -92,6 +135,11 @@ export class FilterHelper {
return data.filter(entity => entity.type === 'group'); return data.filter(entity => entity.type === 'group');
} }
/**
* Applies a search filter to the data. Uses fuzzy search if enabled.
* @param {any[]} data The data to filter.
* @returns {any[]} The filtered data.
*/
searchFilter(data) { searchFilter(data) {
if (!this.filterData[FILTER_TYPES.SEARCH]) { if (!this.filterData[FILTER_TYPES.SEARCH]) {
return data; return data;
@ -122,6 +170,12 @@ export class FilterHelper {
return data.filter(entity => getIsValidSearch(entity)); return data.filter(entity => getIsValidSearch(entity));
} }
/**
* Sets the filter data for the given filter type.
* @param {string} filterType The filter type to set data for.
* @param {any} data The data to set.
* @param {boolean} suppressDataChanged Whether to suppress the data changed callback.
*/
setFilterData(filterType, data, suppressDataChanged = false) { setFilterData(filterType, data, suppressDataChanged = false) {
const oldData = this.filterData[filterType]; const oldData = this.filterData[filterType];
this.filterData[filterType] = data; this.filterData[filterType] = data;
@ -132,10 +186,19 @@ export class FilterHelper {
} }
} }
/**
* Gets the filter data for the given filter type.
* @param {string} filterType The filter type to get data for.
*/
getFilterData(filterType) { getFilterData(filterType) {
return this.filterData[filterType]; return this.filterData[filterType];
} }
/**
* Applies all filters to the given data.
* @param {any[]} data The data to filter.
* @returns {any[]} The filtered data.
*/
applyFilters(data) { applyFilters(data) {
return Object.values(this.filterFunctions) return Object.values(this.filterFunctions)
.reduce((data, fn) => fn(data), data); .reduce((data, fn) => fn(data), data);

View File

@ -9,7 +9,7 @@ import {
saveBase64AsFile, saveBase64AsFile,
PAGINATION_TEMPLATE, PAGINATION_TEMPLATE,
} from './utils.js'; } from './utils.js';
import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap } from "./RossAscends-mods.js"; import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from "./RossAscends-mods.js";
import { loadMovingUIState, sortEntitiesList } from './power-user.js'; import { loadMovingUIState, sortEntitiesList } from './power-user.js';
import { import {
@ -202,7 +202,7 @@ function getFirstCharacterMessage(character) {
mes["is_system"] = false; mes["is_system"] = false;
mes["name"] = character.name; mes["name"] = character.name;
mes["is_name"] = true; mes["is_name"] = true;
mes["send_date"] = humanizedDateTime(); mes["send_date"] = getMessageTimeStamp();
mes["original_avatar"] = character.avatar; mes["original_avatar"] = character.avatar;
mes["extra"] = { "gen_id": Date.now() * Math.random() * 1000000 }; mes["extra"] = { "gen_id": Date.now() * Math.random() * 1000000 };
mes["mes"] = messageText mes["mes"] = messageText
@ -463,7 +463,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
is_group_generating = true; is_group_generating = true;
setCharacterName(''); setCharacterName('');
setCharacterId(undefined); setCharacterId(undefined);
const userInput = $("#send_textarea").val(); const userInput = String($("#send_textarea").val());
if (typingIndicator.length === 0 && !isStreamingEnabled()) { if (typingIndicator.length === 0 && !isStreamingEnabled()) {
typingIndicator = $( typingIndicator = $(
@ -983,11 +983,9 @@ function printGroupCandidates() {
const storageKey = 'GroupCandidates_PerPage'; const storageKey = 'GroupCandidates_PerPage';
$("#rm_group_add_members_pagination").pagination({ $("#rm_group_add_members_pagination").pagination({
dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }), dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }),
pageSize: 5,
pageRange: 1, pageRange: 1,
position: 'top', position: 'top',
showPageNumbers: false, showPageNumbers: false,
showSizeChanger: false,
prevText: '<', prevText: '<',
nextText: '>', nextText: '>',
formatNavigator: PAGINATION_TEMPLATE, formatNavigator: PAGINATION_TEMPLATE,
@ -1011,11 +1009,9 @@ function printGroupMembers() {
const storageKey = 'GroupMembers_PerPage'; const storageKey = 'GroupMembers_PerPage';
$("#rm_group_members_pagination").pagination({ $("#rm_group_members_pagination").pagination({
dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }), dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
pageSize: 5,
pageRange: 1, pageRange: 1,
position: 'top', position: 'top',
showPageNumbers: false, showPageNumbers: false,
showSizeChanger: false,
prevText: '<', prevText: '<',
nextText: '>', nextText: '>',
formatNavigator: PAGINATION_TEMPLATE, formatNavigator: PAGINATION_TEMPLATE,
@ -1320,7 +1316,7 @@ function openCharacterDefinition(characterSelect) {
} }
function filterGroupMembers() { function filterGroupMembers() {
const searchValue = $(this).val().toLowerCase(); const searchValue = String($(this).val()).toLowerCase();
groupCandidatesFilter.setFilterData(FILTER_TYPES.SEARCH, searchValue); groupCandidatesFilter.setFilterData(FILTER_TYPES.SEARCH, searchValue);
} }
@ -1390,7 +1386,7 @@ export async function createNewGroupChat(groupId) {
group.chat_metadata = {}; group.chat_metadata = {};
updateChatMetadata(group.chat_metadata, true); updateChatMetadata(group.chat_metadata, true);
await editGroup(group.id, true); await editGroup(group.id, true, false);
await getGroupChat(group.id); await getGroupChat(group.id);
} }

75
public/scripts/i18n.js Normal file
View File

@ -0,0 +1,75 @@
import { waitUntilCondition } from "./utils.js";
const storageKey = "language";
export const localeData = await fetch("i18n.json").then(response => response.json());
export function applyLocale(root = document) {
const overrideLanguage = localStorage.getItem("language");
var language = overrideLanguage || navigator.language || navigator.userLanguage;
language = language.toLowerCase();
//load the appropriate language file
if (localeData.lang.indexOf(language) < 0) language = "en";
const $root = root instanceof Document ? $(root) : $(new DOMParser().parseFromString(root, "text/html"));
//find all the elements with `data-i18n` attribute
$root.find("[data-i18n]").each(function () {
//read the translation from the language data
const keys = $(this).data("i18n").split(';'); // Multi-key entries are ; delimited
for (const key of keys) {
const attributeMatch = key.match(/\[(\S+)\](.+)/); // [attribute]key
if (attributeMatch) { // attribute-tagged key
const localizedValue = localeData?.[language]?.[attributeMatch[2]];
if (localizedValue) {
$(this).attr(attributeMatch[1], localizedValue);
}
} else { // No attribute tag, treat as 'text'
const localizedValue = localeData?.[language]?.[key];
if (localizedValue) {
$(this).text(localizedValue);
}
}
}
});
if (root !== document) {
return $root.get(0).body.innerHTML;
}
}
function addLanguagesToDropdown() {
if (!Array.isArray(localeData?.lang)) {
return;
}
for (const lang of localeData.lang) {
const option = document.createElement('option');
option.value = lang;
option.innerText = lang;
$('#ui_language_select').append(option);
}
const selectedLanguage = localStorage.getItem(storageKey);
if (selectedLanguage) {
$('#ui_language_select').val(selectedLanguage);
}
}
jQuery(async () => {
waitUntilCondition(() => !!localeData);
window["applyLocale"] = applyLocale;
applyLocale();
addLanguagesToDropdown();
$('#ui_language_select').on('change', async function () {
const language = String($(this).val());
if (language) {
localStorage.setItem(storageKey, language);
} else {
localStorage.removeItem(storageKey);
}
location.reload();
});
});

View File

@ -4,6 +4,9 @@ import { saveSettingsDebounced, substituteParams } from "../script.js";
import { selected_group } from "./group-chats.js"; import { selected_group } from "./group-chats.js";
import { power_user } from "./power-user.js"; import { power_user } from "./power-user.js";
/**
* @type {any[]} Instruct mode presets.
*/
export let instruct_presets = []; export let instruct_presets = [];
const controls = [ const controls = [
@ -116,6 +119,11 @@ export function autoSelectInstructPreset(modelId) {
* @returns {string[]} Array of instruct mode stopping strings. * @returns {string[]} Array of instruct mode stopping strings.
*/ */
export function getInstructStoppingSequences() { export function getInstructStoppingSequences() {
/**
* Adds instruct mode sequence to the result array.
* @param {string} sequence Sequence string.
* @returns {void}
*/
function addInstructSequence(sequence) { function addInstructSequence(sequence) {
// Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string // Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string
// But it's a problem for Metharme which doesn't use newlines to separate them. // But it's a problem for Metharme which doesn't use newlines to separate them.
@ -215,6 +223,7 @@ export function formatInstructModeExamples(mesExamples, name1, name2) {
* @param {string} promptBias Prompt bias string. * @param {string} promptBias Prompt bias string.
* @param {string} name1 User name. * @param {string} name1 User name.
* @param {string} name2 Character name. * @param {string} name2 Character name.
* @returns {string} Formatted instruct mode last prompt line.
*/ */
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2) { export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2) {
const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups); const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups);
@ -258,7 +267,7 @@ jQuery(() => {
return; return;
} }
power_user.instruct.preset = name; power_user.instruct.preset = String(name);
controls.forEach(control => { controls.forEach(control => {
if (preset[control.property] !== undefined) { if (preset[control.property] !== undefined) {
power_user.instruct[control.property] = preset[control.property]; power_user.instruct[control.property] = preset[control.property];

View File

@ -75,18 +75,18 @@ function loadKoboldSettings(preset) {
} }
} }
function getKoboldGenerationData(finalPromt, this_settings, this_amount_gen, this_max_context, isImpersonate, type) { function getKoboldGenerationData(finalPrompt, this_settings, this_amount_gen, this_max_context, isImpersonate, type) {
const sampler_order = kai_settings.sampler_order || this_settings.sampler_order; const sampler_order = kai_settings.sampler_order || this_settings.sampler_order;
let generate_data = { let generate_data = {
prompt: finalPromt, prompt: finalPrompt,
gui_settings: false, gui_settings: false,
sampler_order: sampler_order, sampler_order: sampler_order,
max_context_length: parseInt(this_max_context), max_context_length: Number(this_max_context),
max_length: this_amount_gen, max_length: this_amount_gen,
rep_pen: parseFloat(kai_settings.rep_pen), rep_pen: Number(kai_settings.rep_pen),
rep_pen_range: parseInt(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,
temperature: parseFloat(kai_settings.temp), temperature: Number(kai_settings.temp),
tfs: kai_settings.tfs, tfs: kai_settings.tfs,
top_a: kai_settings.top_a, top_a: kai_settings.top_a,
top_k: kai_settings.top_k, top_k: kai_settings.top_k,
@ -223,16 +223,30 @@ const sliders = [
} }
]; ];
/**
* Determines if the Kobold stop sequence can be used with the given version.
* @param {string} version KoboldAI version to check.
* @returns {boolean} True if the Kobold stop sequence can be used, false otherwise.
*/
function canUseKoboldStopSequence(version) { function canUseKoboldStopSequence(version) {
return (version || '0.0.0').localeCompare(MIN_STOP_SEQUENCE_VERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1; return (version || '0.0.0').localeCompare(MIN_STOP_SEQUENCE_VERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} }
/**
* Determines if the Kobold streaming API can be used with the given version.
* @param {{ result: string; version: string; }} koboldVersion KoboldAI version object.
* @returns {boolean} True if the Kobold streaming API can be used, false otherwise.
*/
function canUseKoboldStreaming(koboldVersion) { function canUseKoboldStreaming(koboldVersion) {
if (koboldVersion && koboldVersion.result == 'KoboldCpp') { if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_STREAMING_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1; return (koboldVersion.version || '0.0').localeCompare(MIN_STREAMING_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false; } else return false;
} }
/**
* Sorts the sampler items by the given order.
* @param {any[]} orderArray Sampler order array.
*/
function sortItemsByOrder(orderArray) { function sortItemsByOrder(orderArray) {
console.debug('Preset samplers order: ' + orderArray); console.debug('Preset samplers order: ' + orderArray);
const $draggableItems = $("#kobold_order"); const $draggableItems = $("#kobold_order");

View File

@ -1,14 +1,14 @@
import { import {
getRequestHeaders, getRequestHeaders,
getStoppingStrings, getStoppingStrings,
getTextTokens,
max_context, max_context,
novelai_setting_names, novelai_setting_names,
saveSettingsDebounced, saveSettingsDebounced,
setGenerationParamsFromPreset setGenerationParamsFromPreset
} from "../script.js"; } from "../script.js";
import { getCfg } from "./extensions/cfg/util.js"; import { getCfgPrompt } from "./extensions/cfg/util.js";
import { MAX_CONTEXT_DEFAULT, tokenizers } from "./power-user.js"; import { MAX_CONTEXT_DEFAULT } from "./power-user.js";
import { getTextTokens, tokenizers } from "./tokenizers.js";
import { import {
getSortableDelay, getSortableDelay,
getStringHash, getStringHash,
@ -395,7 +395,11 @@ function getBadWordPermutations(text) {
return result; return result;
} }
export function getNovelGenerationData(finalPrompt, this_settings, this_amount_gen, isImpersonate) { export function getNovelGenerationData(finalPrompt, this_settings, this_amount_gen, isImpersonate, cfgValues) {
if (cfgValues.guidanceScale && cfgValues.guidanceScale?.value !== 1) {
cfgValues.negativePrompt = (getCfgPrompt(cfgValues.guidanceScale, true))?.value;
}
const clio = nai_settings.model_novel.includes('clio'); const clio = nai_settings.model_novel.includes('clio');
const kayra = nai_settings.model_novel.includes('kayra'); const kayra = nai_settings.model_novel.includes('kayra');
@ -410,7 +414,6 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g
: undefined; : undefined;
const prefix = selectPrefix(nai_settings.prefix, finalPrompt); const prefix = selectPrefix(nai_settings.prefix, finalPrompt);
const cfgSettings = getCfg();
let logitBias = []; let logitBias = [];
if (tokenizerType !== tokenizers.NONE && Array.isArray(nai_settings.logit_bias) && nai_settings.logit_bias.length) { if (tokenizerType !== tokenizers.NONE && Array.isArray(nai_settings.logit_bias) && nai_settings.logit_bias.length) {
@ -437,8 +440,8 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g
"typical_p": parseFloat(nai_settings.typical_p), "typical_p": parseFloat(nai_settings.typical_p),
"mirostat_lr": parseFloat(nai_settings.mirostat_lr), "mirostat_lr": parseFloat(nai_settings.mirostat_lr),
"mirostat_tau": parseFloat(nai_settings.mirostat_tau), "mirostat_tau": parseFloat(nai_settings.mirostat_tau),
"cfg_scale": cfgSettings?.guidanceScale ?? parseFloat(nai_settings.cfg_scale), "cfg_scale": cfgValues?.guidanceScale?.value ?? parseFloat(nai_settings.cfg_scale),
"cfg_uc": cfgSettings?.negativePrompt ?? nai_settings.cfg_uc ?? "", "cfg_uc": cfgValues?.negativePrompt ?? nai_settings.cfg_uc ?? "",
"phrase_rep_pen": nai_settings.phrase_rep_pen, "phrase_rep_pen": nai_settings.phrase_rep_pen,
"stop_sequences": stopSequences, "stop_sequences": stopSequences,
"bad_words_ids": badWordIds, "bad_words_ids": badWordIds,

View File

@ -48,10 +48,10 @@ import {
delay, delay,
download, download,
getFileText, getSortableDelay, getFileText, getSortableDelay,
getStringHash,
parseJsonFile, parseJsonFile,
stringFormat, stringFormat,
} from "./utils.js"; } from "./utils.js";
import { countTokensOpenAI } from "./tokenizers.js";
export { export {
is_get_status_openai, is_get_status_openai,
@ -67,7 +67,6 @@ export {
sendOpenAIRequest, sendOpenAIRequest,
setOpenAIOnlineStatus, setOpenAIOnlineStatus,
getChatCompletionModel, getChatCompletionModel,
countTokens,
TokenHandler, TokenHandler,
IdentifierNotFoundError, IdentifierNotFoundError,
Message, Message,
@ -109,8 +108,8 @@ const max_4k = 4095;
const max_8k = 8191; const max_8k = 8191;
const max_16k = 16383; const max_16k = 16383;
const max_32k = 32767; const max_32k = 32767;
const scale_max = 7900; // Probably more. Save some for the system prompt defined on Scale site. const scale_max = 8191;
const claude_max = 8000; // We have a proper tokenizer, so theoretically could be larger (up to 9k) const claude_max = 9000; // We have a proper tokenizer, so theoretically could be larger (up to 9k)
const palm2_max = 7500; // The real context window is 8192, spare some for padding due to using turbo tokenizer const palm2_max = 7500; // The real context window is 8192, spare some for padding due to using turbo tokenizer
const claude_100k_max = 99000; const claude_100k_max = 99000;
let ai21_max = 9200; //can easily fit 9k gpt tokens because j2's tokenizer is efficient af let ai21_max = 9200; //can easily fit 9k gpt tokens because j2's tokenizer is efficient af
@ -124,40 +123,6 @@ const openrouter_website_model = 'OR_Website';
let biasCache = undefined; let biasCache = undefined;
let model_list = []; let model_list = [];
const objectStore = new localforage.createInstance({ name: "SillyTavern_ChatCompletions" });
let tokenCache = {};
async function loadTokenCache() {
try {
console.debug('Chat Completions: loading token cache')
tokenCache = await objectStore.getItem('tokenCache') || {};
} catch (e) {
console.log('Chat Completions: unable to load token cache, using default value', e);
tokenCache = {};
}
}
async function saveTokenCache() {
try {
console.debug('Chat Completions: saving token cache')
await objectStore.setItem('tokenCache', tokenCache);
} catch (e) {
console.log('Chat Completions: unable to save token cache', e);
}
}
async function resetTokenCache() {
try {
console.debug('Chat Completions: resetting token cache');
Object.keys(tokenCache).forEach(key => delete tokenCache[key]);
await objectStore.removeItem('tokenCache');
} catch (e) {
console.log('Chat Completions: unable to reset token cache', e);
}
}
window['resetTokenCache'] = resetTokenCache;
export const chat_completion_sources = { export const chat_completion_sources = {
OPENAI: 'openai', OPENAI: 'openai',
@ -219,6 +184,7 @@ const default_settings = {
assistant_prefill: '', assistant_prefill: '',
use_ai21_tokenizer: false, use_ai21_tokenizer: false,
exclude_assistant: false, exclude_assistant: false,
use_alt_scale: false,
}; };
const oai_settings = { const oai_settings = {
@ -261,15 +227,12 @@ const oai_settings = {
assistant_prefill: '', assistant_prefill: '',
use_ai21_tokenizer: false, use_ai21_tokenizer: false,
exclude_assistant: false, exclude_assistant: false,
use_alt_scale: false,
}; };
let openai_setting_names; let openai_setting_names;
let openai_settings; let openai_settings;
export function getTokenCountOpenAI(text) {
const message = { role: 'system', content: text };
return countTokens(message, true);
}
let promptManager = null; let promptManager = null;
@ -869,8 +832,6 @@ function prepareOpenAIMessages({
const chat = chatCompletion.getChat(); const chat = chatCompletion.getChat();
openai_messages_count = chat.filter(x => x?.role === "user" || x?.role === "assistant")?.length || 0; openai_messages_count = chat.filter(x => x?.role === "user" || x?.role === "assistant")?.length || 0;
// Save token cache to IndexedDB storage (async, no need to await)
saveTokenCache();
return [chat, promptManager.tokenHandler.counts]; return [chat, promptManager.tokenHandler.counts];
} }
@ -1082,6 +1043,47 @@ function saveModelList(data) {
} }
} }
async function sendAltScaleRequest(openai_msgs_tosend, logit_bias, signal) {
const generate_url = '/generate_altscale';
let firstSysMsgs = []
for (let msg of openai_msgs_tosend) {
if (msg.role === 'system') {
firstSysMsgs.push(substituteParams(msg.name ? msg.name + ": " + msg.content : msg.content));
} else {
break;
}
}
let subsequentMsgs = openai_msgs_tosend.slice(firstSysMsgs.length);
const joinedSysMsgs = substituteParams(firstSysMsgs.join("\n"));
const joinedSubsequentMsgs = subsequentMsgs.reduce((acc, obj) => {
return acc + obj.role + ": " + obj.content + "\n";
}, "");
openai_msgs_tosend = substituteParams(joinedSubsequentMsgs);
const generate_data = {
sysprompt: joinedSysMsgs,
prompt: openai_msgs_tosend,
temp: parseFloat(oai_settings.temp_openai),
top_p: parseFloat(oai_settings.top_p_openai),
max_tokens: parseFloat(oai_settings.openai_max_tokens),
logit_bias: logit_bias,
}
const response = await fetch(generate_url, {
method: 'POST',
body: JSON.stringify(generate_data),
headers: getRequestHeaders(),
signal: signal
});
const data = await response.json();
return data.output;
}
async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
// Provide default abort signal // Provide default abort signal
if (!signal) { if (!signal) {
@ -1118,7 +1120,7 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
return sendWindowAIRequest(openai_msgs_tosend, signal, stream); return sendWindowAIRequest(openai_msgs_tosend, signal, stream);
} }
const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.OPENROUTER]; const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.SCALE];
if (oai_settings.bias_preset_selected if (oai_settings.bias_preset_selected
&& logitBiasSources.includes(oai_settings.chat_completion_source) && logitBiasSources.includes(oai_settings.chat_completion_source)
&& Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected]) && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected])
@ -1127,6 +1129,10 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
biasCache = logit_bias; biasCache = logit_bias;
} }
if (isScale && oai_settings.use_alt_scale) {
return sendAltScaleRequest(openai_msgs_tosend, logit_bias, signal)
}
const model = getChatCompletionModel(); const model = getChatCompletionModel();
const generate_data = { const generate_data = {
"messages": openai_msgs_tosend, "messages": openai_msgs_tosend,
@ -1363,63 +1369,8 @@ class TokenHandler {
} }
} }
function countTokens(messages, full = false) {
let shouldTokenizeAI21 = oai_settings.chat_completion_source === chat_completion_sources.AI21 && oai_settings.use_ai21_tokenizer;
let chatId = 'undefined';
try { const tokenHandler = new TokenHandler(countTokensOpenAI);
if (selected_group) {
chatId = groups.find(x => x.id == selected_group)?.chat_id;
}
else if (this_chid) {
chatId = characters[this_chid].chat;
}
} catch {
console.log('No character / group selected. Using default cache item');
}
if (typeof tokenCache[chatId] !== 'object') {
tokenCache[chatId] = {};
}
if (!Array.isArray(messages)) {
messages = [messages];
}
let token_count = -1;
for (const message of messages) {
const model = getTokenizerModel();
const hash = getStringHash(JSON.stringify(message));
const cacheKey = `${model}-${hash}`;
const cachedCount = tokenCache[chatId][cacheKey];
if (typeof cachedCount === 'number') {
token_count += cachedCount;
}
else {
jQuery.ajax({
async: false,
type: 'POST', //
url: shouldTokenizeAI21 ? '/tokenize_ai21' : `/tokenize_openai?model=${model}`,
data: JSON.stringify([message]),
dataType: "json",
contentType: "application/json",
success: function (data) {
token_count += Number(data.token_count);
tokenCache[chatId][cacheKey] = Number(data.token_count);
}
});
}
}
if (!full) token_count -= 2;
return token_count;
}
const tokenHandler = new TokenHandler(countTokens);
// Thrown by ChatCompletion when a requested prompt couldn't be found. // Thrown by ChatCompletion when a requested prompt couldn't be found.
class IdentifierNotFoundError extends Error { class IdentifierNotFoundError extends Error {
@ -1856,62 +1807,6 @@ class ChatCompletion {
} }
} }
export function getTokenizerModel() {
// OpenAI models always provide their own tokenizer
if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
return oai_settings.openai_model;
}
const turboTokenizer = 'gpt-3.5-turbo';
const gpt4Tokenizer = 'gpt-4';
const gpt2Tokenizer = 'gpt2';
const claudeTokenizer = 'claude';
// Assuming no one would use it for different models.. right?
if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
return gpt4Tokenizer;
}
// Select correct tokenizer for WindowAI proxies
if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI && oai_settings.windowai_model) {
if (oai_settings.windowai_model.includes('gpt-4')) {
return gpt4Tokenizer;
}
else if (oai_settings.windowai_model.includes('gpt-3.5-turbo')) {
return turboTokenizer;
}
else if (oai_settings.windowai_model.includes('claude')) {
return claudeTokenizer;
}
else if (oai_settings.windowai_model.includes('GPT-NeoXT')) {
return gpt2Tokenizer;
}
}
// And for OpenRouter (if not a site model, then it's impossible to determine the tokenizer)
if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model) {
if (oai_settings.openrouter_model.includes('gpt-4')) {
return gpt4Tokenizer;
}
else if (oai_settings.openrouter_model.includes('gpt-3.5-turbo')) {
return turboTokenizer;
}
else if (oai_settings.openrouter_model.includes('claude')) {
return claudeTokenizer;
}
else if (oai_settings.openrouter_model.includes('GPT-NeoXT')) {
return gpt2Tokenizer;
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
return claudeTokenizer;
}
// Default to Turbo 3.5
return turboTokenizer;
}
function loadOpenAISettings(data, settings) { function loadOpenAISettings(data, settings) {
openai_setting_names = data.openai_setting_names; openai_setting_names = data.openai_setting_names;
openai_settings = data.openai_settings; openai_settings = data.openai_settings;
@ -1971,6 +1866,7 @@ function loadOpenAISettings(data, settings) {
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model; if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
if (settings.use_ai21_tokenizer !== undefined) { oai_settings.use_ai21_tokenizer = !!settings.use_ai21_tokenizer; oai_settings.use_ai21_tokenizer ? ai21_max = 8191 : ai21_max = 9200; } if (settings.use_ai21_tokenizer !== undefined) { oai_settings.use_ai21_tokenizer = !!settings.use_ai21_tokenizer; oai_settings.use_ai21_tokenizer ? ai21_max = 8191 : ai21_max = 9200; }
if (settings.exclude_assistant !== undefined) oai_settings.exclude_assistant = !!settings.exclude_assistant; if (settings.exclude_assistant !== undefined) oai_settings.exclude_assistant = !!settings.exclude_assistant;
if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
$('#stream_toggle').prop('checked', oai_settings.stream_openai); $('#stream_toggle').prop('checked', oai_settings.stream_openai);
$('#api_url_scale').val(oai_settings.api_url_scale); $('#api_url_scale').val(oai_settings.api_url_scale);
$('#openai_proxy_password').val(oai_settings.proxy_password); $('#openai_proxy_password').val(oai_settings.proxy_password);
@ -2001,6 +1897,7 @@ function loadOpenAISettings(data, settings) {
$('#openai_external_category').toggle(oai_settings.show_external_models); $('#openai_external_category').toggle(oai_settings.show_external_models);
$('#use_ai21_tokenizer').prop('checked', oai_settings.use_ai21_tokenizer); $('#use_ai21_tokenizer').prop('checked', oai_settings.use_ai21_tokenizer);
$('#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);
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);
@ -2199,6 +2096,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
assistant_prefill: settings.assistant_prefill, assistant_prefill: settings.assistant_prefill,
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,
}; };
const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, { const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, {
@ -2536,6 +2434,7 @@ function onSettingsPresetChange() {
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', false],
exclude_assistant: ['#exclude_assistant', 'exclude_assistant', false], exclude_assistant: ['#exclude_assistant', 'exclude_assistant', false],
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', false],
}; };
const presetName = $('#settings_perset_openai').find(":selected").text(); const presetName = $('#settings_perset_openai').find(":selected").text();
@ -2831,20 +2730,31 @@ async function onConnectButtonClick(e) {
if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) { if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
const api_key_scale = $('#api_key_scale').val().trim(); const api_key_scale = $('#api_key_scale').val().trim();
const scale_cookie = $('#scale_cookie').val().trim();
if (api_key_scale.length) { if (api_key_scale.length) {
await writeSecret(SECRET_KEYS.SCALE, api_key_scale); await writeSecret(SECRET_KEYS.SCALE, api_key_scale);
} }
if (!oai_settings.api_url_scale) { if (scale_cookie.length) {
await writeSecret(SECRET_KEYS.SCALE_COOKIE, scale_cookie);
}
if (!oai_settings.api_url_scale && !oai_settings.use_alt_scale) {
console.log('No API URL saved for Scale'); console.log('No API URL saved for Scale');
return; return;
} }
if (!secret_state[SECRET_KEYS.SCALE]) { if (!secret_state[SECRET_KEYS.SCALE] && !oai_settings.use_alt_scale) {
console.log('No secret key saved for Scale'); console.log('No secret key saved for Scale');
return; return;
} }
if (!secret_state[SECRET_KEYS.SCALE_COOKIE] && oai_settings.use_alt_scale) {
console.log("No cookie set for Scale");
return;
}
} }
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) { if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
@ -2958,11 +2868,25 @@ function onProxyPasswordShowClick() {
$(this).toggleClass('fa-eye-slash fa-eye'); $(this).toggleClass('fa-eye-slash fa-eye');
} }
$(document).ready(async function () { function updateScaleForm() {
await loadTokenCache(); if (oai_settings.use_alt_scale) {
$('#normal_scale_form').css('display', 'none');
$('#alt_scale_form').css('display', '');
} else {
$('#normal_scale_form').css('display', '');
$('#alt_scale_form').css('display', 'none');
}
}
$(document).ready(async function () {
$('#test_api_button').on('click', testApiConnection); $('#test_api_button').on('click', testApiConnection);
$('#scale-alt').on('change', function () {
oai_settings.use_alt_scale = !!$('#scale-alt').prop('checked');
saveSettingsDebounced();
updateScaleForm();
});
$(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').text(Number($(this).val()).toFixed(2));

View File

@ -23,6 +23,7 @@ import {
import { loadInstructMode } from "./instruct-mode.js"; import { loadInstructMode } from "./instruct-mode.js";
import { registerSlashCommand } from "./slash-commands.js"; import { registerSlashCommand } from "./slash-commands.js";
import { tokenizers } from "./tokenizers.js";
import { delay } from "./utils.js"; import { delay } from "./utils.js";
@ -35,7 +36,6 @@ export {
fixMarkdown, fixMarkdown,
power_user, power_user,
pygmalion_options, pygmalion_options,
tokenizers,
send_on_enter_options, send_on_enter_options,
}; };
@ -63,17 +63,6 @@ const pygmalion_options = {
ENABLED: 1, ENABLED: 1,
} }
const tokenizers = {
NONE: 0,
GPT3: 1,
CLASSIC: 2,
LLAMA: 3,
NERD: 4,
NERD2: 5,
API: 6,
BEST_MATCH: 99,
}
const send_on_enter_options = { const send_on_enter_options = {
DISABLED: -1, DISABLED: -1,
AUTO: 0, AUTO: 0,
@ -207,7 +196,6 @@ let movingUIPresets = [];
let context_presets = []; let context_presets = [];
const storage_keys = { const storage_keys = {
ui_language: "language",
fast_ui_mode: "TavernAI_fast_ui_mode", fast_ui_mode: "TavernAI_fast_ui_mode",
avatar_style: "TavernAI_avatar_style", avatar_style: "TavernAI_avatar_style",
chat_display: "TavernAI_chat_display", chat_display: "TavernAI_chat_display",
@ -247,29 +235,42 @@ function playMessageSound() {
} }
const audio = document.getElementById('audio_message_sound'); const audio = document.getElementById('audio_message_sound');
if (audio instanceof HTMLAudioElement) {
audio.volume = 0.8; audio.volume = 0.8;
audio.pause(); audio.pause();
audio.currentTime = 0; audio.currentTime = 0;
audio.play(); audio.play();
} }
}
/**
* Replaces consecutive newlines with a single newline.
* @param {string} x String to be processed.
* @returns {string} Processed string.
* @example
* collapseNewlines("\n\n\n"); // "\n"
*/
function collapseNewlines(x) { function collapseNewlines(x) {
return x.replaceAll(/\n+/g, "\n"); return x.replaceAll(/\n+/g, "\n");
} }
/**
* Fix formatting problems in markdown.
* @param {string} text Text to be processed.
* @returns {string} Processed text.
* @example
* "^example * text*\n" // "^example *text*\n"
* "^*example * text\n"// "^*example* text\n"
* "^example *text *\n" // "^example *text*\n"
* "^* example * text\n" // "^*example* text\n"
* // take note that the side you move the asterisk depends on where its pairing is
* // i.e. both of the following strings have the same broken asterisk ' * ',
* // but you move the first to the left and the second to the right, to match the non-broken asterisk
* "^example * text*\n" // "^*example * text\n"
* // and you HAVE to handle the cases where multiple pairs of asterisks exist in the same line
* "^example * text* * harder problem *\n" // "^example *text* *harder problem*\n"
*/
function fixMarkdown(text) { function fixMarkdown(text) {
// fix formatting problems in markdown
// e.g.:
// "^example * text*\n" -> "^example *text*\n"
// "^*example * text\n" -> "^*example* text\n"
// "^example *text *\n" -> "^example *text*\n"
// "^* example * text\n" -> "^*example* text\n"
// take note that the side you move the asterisk depends on where its pairing is
// i.e. both of the following strings have the same broken asterisk ' * ',
// but you move the first to the left and the second to the right, to match the non-broken asterisk "^example * text*\n" "^*example * text\n"
// and you HAVE to handle the cases where multiple pairs of asterisks exist in the same line
// i.e. "^example * text* * harder problem *\n" -> "^example *text* *harder problem*\n"
// Find pairs of formatting characters and capture the text in between them // Find pairs of formatting characters and capture the text in between them
const format = /([\*_]{1,2})([\s\S]*?)\1/gm; const format = /([\*_]{1,2})([\s\S]*?)\1/gm;
let matches = []; let matches = [];
@ -899,7 +900,7 @@ function loadContextSettings() {
}); });
$('#context_presets').on('change', function () { $('#context_presets').on('change', function () {
const name = $(this).find(':selected').val(); const name = String($(this).find(':selected').val());
const preset = context_presets.find(x => x.name === name); const preset = context_presets.find(x => x.name === name);
if (!preset) { if (!preset) {
@ -1020,6 +1021,10 @@ const compareFunc = (first, second) => {
} }
}; };
/**
* Sorts an array of entities based on the current sort settings
* @param {any[]} entities An array of objects with an `item` property
*/
function sortEntitiesList(entities) { function sortEntitiesList(entities) {
if (power_user.sort_field == undefined || entities.length === 0) { if (power_user.sort_field == undefined || entities.length === 0) {
return; return;
@ -1027,6 +1032,7 @@ function sortEntitiesList(entities) {
entities.sort((a, b) => sortFunc(a.item, b.item)); entities.sort((a, b) => sortFunc(a.item, b.item));
} }
async function saveTheme() { async function saveTheme() {
const name = await callPopup('Enter a theme preset name:', 'input'); const name = await callPopup('Enter a theme preset name:', 'input');
@ -1250,8 +1256,8 @@ async function doDelMode(_, text) {
if (text) { if (text) {
await delay(300) //same as above, need event signal for 'entered del mode' await delay(300) //same as above, need event signal for 'entered del mode'
console.debug('parsing msgs to del') console.debug('parsing msgs to del')
let numMesToDel = Number(text).toFixed(0) let numMesToDel = Number(text);
let lastMesID = $('.last_mes').attr('mesid') let lastMesID = Number($('.last_mes').attr('mesid'));
let oldestMesIDToDel = lastMesID - numMesToDel + 1; let oldestMesIDToDel = lastMesID - numMesToDel + 1;
//disallow targeting first message //disallow targeting first message
@ -1277,26 +1283,6 @@ function doResetPanels() {
$("#movingUIreset").trigger('click'); $("#movingUIreset").trigger('click');
} }
function addLanguagesToDropdown() {
$.getJSON('i18n.json', function (data) {
if (!Array.isArray(data?.lang)) {
return;
}
for (const lang of data.lang) {
const option = document.createElement('option');
option.value = lang;
option.innerText = lang;
$('#ui_language_select').append(option);
}
const selectedLanguage = localStorage.getItem(storage_keys.ui_language);
if (selectedLanguage) {
$('#ui_language_select').val(selectedLanguage);
}
});
}
function setAvgBG() { function setAvgBG() {
const bgimg = new Image(); const bgimg = new Image();
bgimg.src = $('#bg1') bgimg.src = $('#bg1')
@ -1348,10 +1334,6 @@ function setAvgBG() {
$("#user-mes-blur-tint-color-picker").attr('color', 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'); $("#user-mes-blur-tint-color-picker").attr('color', 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')');
} */ } */
function getAverageRGB(imgEl) { function getAverageRGB(imgEl) {
var blockSize = 5, // only visit every 5 pixels var blockSize = 5, // only visit every 5 pixels
@ -1396,6 +1378,13 @@ function setAvgBG() {
} }
/**
* Converts an HSL color value to RGB.
* @param {number} h Hue value
* @param {number} s Saturation value
* @param {number} l Luminance value
* @return {Array} The RGB representation
*/
function hslToRgb(h, s, l) { function hslToRgb(h, s, l) {
const hueToRgb = (p, q, t) => { const hueToRgb = (p, q, t) => {
if (t < 0) t += 1; if (t < 0) t += 1;
@ -1437,7 +1426,7 @@ function setAvgBG() {
console.log(`rLum ${rLuminance}, gLum ${gLuminance}, bLum ${bLuminance}`) console.log(`rLum ${rLuminance}, gLum ${gLuminance}, bLum ${bLuminance}`)
return 0.2126 * rLuminance + 0.7152 * gLuminance + 0.0722 * bLuminance; return 0.2126 * Number(rLuminance) + 0.7152 * Number(gLuminance) + 0.0722 * Number(bLuminance);
} }
//this version keeps BG and main text in same hue //this version keeps BG and main text in same hue
@ -1620,13 +1609,13 @@ $(document).ready(() => {
}); });
$("#markdown_escape_strings").on('input', function () { $("#markdown_escape_strings").on('input', function () {
power_user.markdown_escape_strings = $(this).val(); power_user.markdown_escape_strings = String($(this).val());
saveSettingsDebounced(); saveSettingsDebounced();
reloadMarkdownProcessor(power_user.render_formulas); reloadMarkdownProcessor(power_user.render_formulas);
}); });
$("#start_reply_with").on('input', function () { $("#start_reply_with").on('input', function () {
power_user.user_prompt_bias = $(this).val(); power_user.user_prompt_bias = String($(this).val());
saveSettingsDebounced(); saveSettingsDebounced();
}); });
@ -1753,7 +1742,7 @@ $(document).ready(() => {
}); });
$("#themes").on('change', function () { $("#themes").on('change', function () {
const themeSelected = $(this).find(':selected').val(); const themeSelected = String($(this).find(':selected').val());
power_user.theme = themeSelected; power_user.theme = themeSelected;
applyTheme(themeSelected); applyTheme(themeSelected);
saveSettingsDebounced(); saveSettingsDebounced();
@ -1761,7 +1750,7 @@ $(document).ready(() => {
$("#movingUIPresets").on('change', async function () { $("#movingUIPresets").on('change', async function () {
console.log('saw MUI preset change') console.log('saw MUI preset change')
const movingUIPresetSelected = $(this).find(':selected').val(); const movingUIPresetSelected = String($(this).find(':selected').val());
power_user.movingUIPreset = movingUIPresetSelected; power_user.movingUIPreset = movingUIPresetSelected;
applyMovingUIPreset(movingUIPresetSelected); applyMovingUIPreset(movingUIPresetSelected);
saveSettingsDebounced(); saveSettingsDebounced();
@ -1821,7 +1810,7 @@ $(document).ready(() => {
}); });
$('#auto_swipe_blacklist').on('input', function () { $('#auto_swipe_blacklist').on('input', function () {
power_user.auto_swipe_blacklist = $(this).val() power_user.auto_swipe_blacklist = String($(this).val())
.split(",") .split(",")
.map(str => str.trim()) .map(str => str.trim())
.filter(str => str); .filter(str => str);
@ -1830,7 +1819,7 @@ $(document).ready(() => {
}); });
$('#auto_swipe_minimum_length').on('input', function () { $('#auto_swipe_minimum_length').on('input', function () {
const number = parseInt($(this).val()); const number = Number($(this).val());
if (!isNaN(number)) { if (!isNaN(number)) {
power_user.auto_swipe_minimum_length = number; power_user.auto_swipe_minimum_length = number;
saveSettingsDebounced(); saveSettingsDebounced();
@ -1838,7 +1827,7 @@ $(document).ready(() => {
}); });
$('#auto_swipe_blacklist_threshold').on('input', function () { $('#auto_swipe_blacklist_threshold').on('input', function () {
const number = parseInt($(this).val()); const number = Number($(this).val());
if (!isNaN(number)) { if (!isNaN(number)) {
power_user.auto_swipe_blacklist_threshold = number; power_user.auto_swipe_blacklist_threshold = number;
saveSettingsDebounced(); saveSettingsDebounced();
@ -1921,35 +1910,35 @@ $(document).ready(() => {
$("#messageTimerEnabled").on("input", function () { $("#messageTimerEnabled").on("input", function () {
const value = !!$(this).prop('checked'); const value = !!$(this).prop('checked');
power_user.timer_enabled = value; power_user.timer_enabled = value;
localStorage.setItem(storage_keys.timer_enabled, power_user.timer_enabled); localStorage.setItem(storage_keys.timer_enabled, String(power_user.timer_enabled));
switchTimer(); switchTimer();
}); });
$("#messageTimestampsEnabled").on("input", function () { $("#messageTimestampsEnabled").on("input", function () {
const value = !!$(this).prop('checked'); const value = !!$(this).prop('checked');
power_user.timestamps_enabled = value; power_user.timestamps_enabled = value;
localStorage.setItem(storage_keys.timestamps_enabled, power_user.timestamps_enabled); localStorage.setItem(storage_keys.timestamps_enabled, String(power_user.timestamps_enabled));
switchTimestamps(); switchTimestamps();
}); });
$("#messageModelIconEnabled").on("input", function () { $("#messageModelIconEnabled").on("input", function () {
const value = !!$(this).prop('checked'); const value = !!$(this).prop('checked');
power_user.timestamp_model_icon = value; power_user.timestamp_model_icon = value;
localStorage.setItem(storage_keys.timestamp_model_icon, power_user.timestamp_model_icon); localStorage.setItem(storage_keys.timestamp_model_icon, String(power_user.timestamp_model_icon));
switchIcons(); switchIcons();
}); });
$("#mesIDDisplayEnabled").on("input", function () { $("#mesIDDisplayEnabled").on("input", function () {
const value = !!$(this).prop('checked'); const value = !!$(this).prop('checked');
power_user.mesIDDisplay_enabled = value; power_user.mesIDDisplay_enabled = value;
localStorage.setItem(storage_keys.mesIDDisplay_enabled, power_user.mesIDDisplay_enabled); localStorage.setItem(storage_keys.mesIDDisplay_enabled, String(power_user.mesIDDisplay_enabled));
switchMesIDDisplay(); switchMesIDDisplay();
}); });
$("#hotswapEnabled").on("input", function () { $("#hotswapEnabled").on("input", function () {
const value = !!$(this).prop('checked'); const value = !!$(this).prop('checked');
power_user.hotswap_enabled = value; power_user.hotswap_enabled = value;
localStorage.setItem(storage_keys.hotswap_enabled, power_user.hotswap_enabled); localStorage.setItem(storage_keys.hotswap_enabled, String(power_user.hotswap_enabled));
switchHotswap(); switchHotswap();
}); });
@ -1995,7 +1984,7 @@ $(document).ready(() => {
}); });
$('#custom_stopping_strings').on('input', function () { $('#custom_stopping_strings').on('input', function () {
power_user.custom_stopping_strings = $(this).val(); power_user.custom_stopping_strings = String($(this).val());
saveSettingsDebounced(); saveSettingsDebounced();
}); });
@ -2025,18 +2014,6 @@ $(document).ready(() => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#ui_language_select').on('change', async function () {
const language = $(this).val();
if (language) {
localStorage.setItem(storage_keys.ui_language, language);
} else {
localStorage.removeItem(storage_keys.ui_language);
}
location.reload();
});
$(window).on('focus', function () { $(window).on('focus', function () {
browser_has_focus = true; browser_has_focus = true;
}); });
@ -2052,5 +2029,4 @@ $(document).ready(() => {
registerSlashCommand('cut', doMesCut, [], ' <span class="monospace">(requred number)</span> cuts the specified message from the chat', true, true); registerSlashCommand('cut', doMesCut, [], ' <span class="monospace">(requred number)</span> cuts the specified message from the chat', true, true);
registerSlashCommand('resetpanels', doResetPanels, ['resetui'], ' resets UI panels to original state.', true, true); registerSlashCommand('resetpanels', doResetPanels, ['resetui'], ' resets UI panels to original state.', true, true);
registerSlashCommand('bgcol', setAvgBG, [], ' WIP test of auto-bg avg coloring', true, true); registerSlashCommand('bgcol', setAvgBG, [], ' WIP test of auto-bg avg coloring', true, true);
addLanguagesToDropdown();
}); });

View File

@ -118,7 +118,7 @@ class PresetManager {
async savePresetAs() { async savePresetAs() {
const popupText = ` const popupText = `
<h3>Preset name:</h3> <h3>Preset name:</h3>
<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>`; ${!this.isNonGenericApi() ? '<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>' : ''}`;
const name = await callPopup(popupText, "input"); const name = await callPopup(popupText, "input");
if (!name) { if (!name) {
@ -131,7 +131,8 @@ class PresetManager {
} }
async savePreset(name, settings) { async savePreset(name, settings) {
const preset = settings ?? this.getPresetSettings(); const preset = settings ?? this.getPresetSettings(name);
const res = await fetch(`/save_preset`, { const res = await fetch(`/save_preset`, {
method: "POST", method: "POST",
headers: getRequestHeaders(), headers: getRequestHeaders(),
@ -220,7 +221,7 @@ class PresetManager {
} }
} }
getPresetSettings() { getPresetSettings(name) {
function getSettingsByApiId(apiId) { function getSettingsByApiId(apiId) {
switch (apiId) { switch (apiId) {
case "koboldhorde": case "koboldhorde":
@ -232,7 +233,7 @@ class PresetManager {
return textgenerationwebui_settings; return textgenerationwebui_settings;
case "instruct": case "instruct":
const preset = deepClone(power_user.instruct); const preset = deepClone(power_user.instruct);
preset['name'] = power_user.instruct.preset; preset['name'] = name || power_user.instruct.preset;
return preset; return preset;
default: default:
console.warn(`Unknown API ID ${apiId}`); console.warn(`Unknown API ID ${apiId}`);
@ -346,7 +347,7 @@ jQuery(async () => {
const selected = $(presetManager.select).find("option:selected"); const selected = $(presetManager.select).find("option:selected");
const name = selected.text(); const name = selected.text();
const preset = presetManager.getPresetSettings(); const preset = presetManager.getPresetSettings(name);
const data = JSON.stringify(preset, null, 4); const data = JSON.stringify(preset, null, 4);
download(data, `${name}.json`, "application/json"); download(data, `${name}.json`, "application/json");
}); });

View File

@ -9,6 +9,7 @@ export const SECRET_KEYS = {
OPENROUTER: 'api_key_openrouter', OPENROUTER: 'api_key_openrouter',
SCALE: 'api_key_scale', SCALE: 'api_key_scale',
AI21: 'api_key_ai21', AI21: 'api_key_ai21',
SCALE_COOKIE: 'scale_cookie',
} }
const INPUT_MAP = { const INPUT_MAP = {
@ -20,6 +21,7 @@ const INPUT_MAP = {
[SECRET_KEYS.OPENROUTER]: '#api_key_openrouter', [SECRET_KEYS.OPENROUTER]: '#api_key_openrouter',
[SECRET_KEYS.SCALE]: '#api_key_scale', [SECRET_KEYS.SCALE]: '#api_key_scale',
[SECRET_KEYS.AI21]: '#api_key_ai21', [SECRET_KEYS.AI21]: '#api_key_ai21',
[SECRET_KEYS.SCALE_COOKIE]: '#scale_cookie',
} }
async function clearSecret() { async function clearSecret() {

View File

@ -22,7 +22,7 @@ import {
reloadCurrentChat, reloadCurrentChat,
sendMessageAsUser, sendMessageAsUser,
} from "../script.js"; } from "../script.js";
import { humanizedDateTime } from "./RossAscends-mods.js"; import { getMessageTimeStamp } from "./RossAscends-mods.js";
import { resetSelectedGroup } from "./group-chats.js"; import { resetSelectedGroup } 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";
@ -327,7 +327,7 @@ async function sendMessageAs(_, text) {
is_user: false, is_user: false,
is_name: true, is_name: true,
is_system: isSystem, is_system: isSystem,
send_date: humanizedDateTime(), send_date: getMessageTimeStamp(),
mes: substituteParams(mesText), mes: substituteParams(mesText),
force_avatar: force_avatar, force_avatar: force_avatar,
original_avatar: original_avatar, original_avatar: original_avatar,
@ -338,8 +338,9 @@ async function sendMessageAs(_, text) {
}; };
chat.push(message); chat.push(message);
addOneMessage(message);
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
addOneMessage(message);
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
saveChatConditional(); saveChatConditional();
} }
@ -358,7 +359,7 @@ async function sendNarratorMessage(_, text) {
is_user: false, is_user: false,
is_name: false, is_name: false,
is_system: isSystem, is_system: isSystem,
send_date: humanizedDateTime(), send_date: getMessageTimeStamp(),
mes: substituteParams(text.trim()), mes: substituteParams(text.trim()),
force_avatar: system_avatar, force_avatar: system_avatar,
extra: { extra: {
@ -369,8 +370,9 @@ async function sendNarratorMessage(_, text) {
}; };
chat.push(message); chat.push(message);
addOneMessage(message);
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
addOneMessage(message);
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
saveChatConditional(); saveChatConditional();
} }
@ -384,7 +386,7 @@ async function sendCommentMessage(_, text) {
is_user: false, is_user: false,
is_name: true, is_name: true,
is_system: true, is_system: true,
send_date: humanizedDateTime(), send_date: getMessageTimeStamp(),
mes: substituteParams(text.trim()), mes: substituteParams(text.trim()),
force_avatar: comment_avatar, force_avatar: comment_avatar,
extra: { extra: {
@ -394,8 +396,9 @@ async function sendCommentMessage(_, text) {
}; };
chat.push(message); chat.push(message);
addOneMessage(message);
await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1));
addOneMessage(message);
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1));
saveChatConditional(); saveChatConditional();
} }

View File

@ -25,13 +25,12 @@ function createStatBlock(statName, statValue) {
* @returns {number} - The stat value if it is a number, otherwise 0. * @returns {number} - The stat value if it is a number, otherwise 0.
*/ */
function verifyStatValue(stat) { function verifyStatValue(stat) {
return isNaN(stat) ? 0 : stat; return isNaN(Number(stat)) ? 0 : Number(stat);
} }
/** /**
* Calculates total stats from character statistics. * Calculates total stats from character statistics.
* *
* @param {Object} charStats - Object containing character statistics.
* @returns {Object} - Object containing total statistics. * @returns {Object} - Object containing total statistics.
*/ */
function calculateTotalStats() { function calculateTotalStats() {

View File

@ -7,7 +7,7 @@ import {
getCharacters, getCharacters,
entitiesFilter, entitiesFilter,
} from "../script.js"; } from "../script.js";
import { FILTER_TYPES } from "./filters.js"; import { FILTER_TYPES, FilterHelper } from "./filters.js";
import { groupCandidatesFilter, selected_group } from "./group-chats.js"; import { groupCandidatesFilter, selected_group } from "./group-chats.js";
import { uuidv4 } from "./utils.js"; import { uuidv4 } from "./utils.js";
@ -24,7 +24,6 @@ export {
importTags, importTags,
}; };
const random_id = () => uuidv4();
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter'; const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter';
@ -49,17 +48,21 @@ const InListActionable = {
} }
const DEFAULT_TAGS = [ const DEFAULT_TAGS = [
{ id: random_id(), name: "Plain Text" }, { id: uuidv4(), name: "Plain Text" },
{ id: random_id(), name: "OpenAI" }, { id: uuidv4(), name: "OpenAI" },
{ id: random_id(), name: "W++" }, { id: uuidv4(), name: "W++" },
{ id: random_id(), name: "Boostyle" }, { id: uuidv4(), name: "Boostyle" },
{ id: random_id(), name: "PList" }, { id: uuidv4(), name: "PList" },
{ id: random_id(), name: "AliChat" }, { id: uuidv4(), name: "AliChat" },
]; ];
let tags = []; let tags = [];
let tag_map = {}; let tag_map = {};
/**
* Applies the favorite filter to the character list.
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
*/
function applyFavFilter(filterHelper) { function applyFavFilter(filterHelper) {
const isSelected = $(this).hasClass('selected'); const isSelected = $(this).hasClass('selected');
const displayFavoritesOnly = !isSelected; const displayFavoritesOnly = !isSelected;
@ -68,6 +71,10 @@ function applyFavFilter(filterHelper) {
filterHelper.setFilterData(FILTER_TYPES.FAV, displayFavoritesOnly); filterHelper.setFilterData(FILTER_TYPES.FAV, displayFavoritesOnly);
} }
/**
* Applies the "is group" filter to the character list.
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
*/
function filterByGroups(filterHelper) { function filterByGroups(filterHelper) {
const isSelected = $(this).hasClass('selected'); const isSelected = $(this).hasClass('selected');
const displayGroupsOnly = !isSelected; const displayGroupsOnly = !isSelected;
@ -253,7 +260,7 @@ async function importTags(imported_char) {
function createNewTag(tagName) { function createNewTag(tagName) {
const tag = { const tag = {
id: random_id(), id: uuidv4(),
name: tagName, name: tagName,
color: '', color: '',
}; };

View File

@ -0,0 +1,21 @@
Text formatting commands:
<ul>
<li><tt>*text*</tt> - displays as <i>italics</i></li>
<li><tt>**text**</tt> - displays as <b>bold</b></li>
<li><tt>***text***</tt> - displays as <b><i>bold italics</i></b></li>
<li><tt>```text```</tt> - displays as a code block (new lines allowed between the backticks)</li>
</ul>
<pre><code> like this</code></pre>
<ul>
<li><tt>`text`</tt> - displays as <code>inline code</code></li>
<li><tt> text</tt> - displays as a blockquote (note the space after >)</li>
<blockquote>like this</blockquote>
<li><tt># text</tt> - displays as a large header (note the space)</li>
<h1>like this</h1>
<li><tt>## text</tt> - displays as a medium header (note the space)</li>
<h2>like this</h2>
<li><tt>### text</tt> - displays as a small header (note the space)</li>
<h3>like this</h3>
<li><tt>$$ text $$</tt> - renders a LaTeX formula (if enabled)</li>
<li><tt>$ text $</tt> - renders an AsciiMath formula (if enabled)</li>
</ul>

View File

@ -0,0 +1,11 @@
Hello there! Please select the help topic you would like to learn more about:
<ul>
<li><a href="#" data-displayHelp="1">Slash Commands</a> (or <tt>/help slash</tt>)</li>
<li><a href="#" data-displayHelp="2">Formatting</a> (or <tt>/help format</tt>)</li>
<li><a href="#" data-displayHelp="3">Hotkeys</a> (or <tt>/help hotkeys</tt>)</li>
<li><a href="#" data-displayHelp="4">&lcub;&lcub;Macros&rcub;&rcub;</a> (or <tt>/help macros</tt>)</li>
</ul>
<br>
<b>
Still got questions left? The <a target="_blank" href="https://docs.sillytavern.app/">Official SillyTavern Documentation Website</a> has much more information!
</b>

View File

@ -0,0 +1,13 @@
Hotkeys/Keybinds:
<ul>
<li><tt>Up</tt> = Edit last message in chat</li>
<li><tt>Ctrl+Up</tt> = Edit last USER message in chat</li>
<li><tt>Left</tt> = swipe left</li>
<li><tt>Right</tt> = swipe right (NOTE: swipe hotkeys are disabled when chatbar has something typed into it)</li>
<li><tt>Ctrl+Left</tt> = view locally stored variables (in the browser console window)</li>
<li><tt>Enter</tt> (with chat bar selected) = send your message to AI</li>
<li><tt>Ctrl+Enter</tt> = Regenerate the last AI response</li>
<li><tt>Escape</tt> = stop AI response generation</li>
<li><tt>Ctrl+Shift+Up</tt> = Scroll to context line</li>
<li><tt>Ctrl+Shift+Down</tt> = Scroll chat to bottom</li>
</ul>

View File

@ -0,0 +1,128 @@
<h3 class="flex-container justifyCenter alignitemscenter">
Prompt Itemization
<div id="showRawPrompt" class="fa-solid fa-square-poll-horizontal menu_button"></div>
</h3>
Tokenizer: {{selectedTokenizer}}<br>
API Used: {{this_main_api}}<br>
<span class="tokenItemizingSubclass">
Only the white numbers really matter. All numbers are estimates.
Grey color items may not have been included in the context due to certain prompt format settings.
</span>
<hr>
<div class="justifyLeft">
<div class="flex-container">
<div class="flex-container flex1 flexFlowColumns flexNoGap wide50p tokenGraph">
<div class="wide100p" style="background-color: grey; height: {{oaiSystemTokensPercentage}}%;"></div>
<div class="wide100p" style="background-color: salmon; height: {{oaiStartTokensPercentage}}%;"></div>
<div class="wide100p" style="background-color: indianred; height: {{storyStringTokensPercentage}}%;"></div>
<div class="wide100p" style="background-color: gold; height: {{worldInfoStringTokensPercentage}}%;"></div>
<div class="wide100p" style="background-color: palegreen; height: {{ActualChatHistoryTokensPercentage}}%;">
</div>
<div class="wide100p" style="background-color: cornflowerblue; height: {{allAnchorsTokensPercentage}}%;">
</div>
<div class="wide100p" style="background-color: mediumpurple; height: {{promptBiasTokensPercentage}}%;">
</div>
</div>
<div class="flex-container wide50p">
<div class="wide100p flex-container flexNoGap flexFlowColumn">
<div class="flex-container wide100p">
<div class="flex1" style="color: grey;">System Info:</div>
<div class=""> {{oaiSystemTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Chat Start: </div>
<div class="tokenItemizingSubclass"> {{oaiStartTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Main: </div>
<div class="tokenItemizingSubclass">{{oaiMainTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Jailbreak: </div>
<div class="tokenItemizingSubclass">{{oaiJailbreakTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- NSFW: </div>
<div class="tokenItemizingSubclass">{{oaiNsfwTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Nudge: </div>
<div class="tokenItemizingSubclass">{{oaiNudgeTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Impersonate: </div>
<div class="tokenItemizingSubclass">{{oaiImpersonateTokens}}</div>
</div>
</div>
<div class="wide100p flex-container flexNoGap flexFlowColumn">
<div class="flex-container wide100p">
<div class="flex1" style="color: indianred;">Prompt Tokens:</div>
<div class=""> {{oaiPromptTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Description: </div>
<div class="tokenItemizingSubclass">{{charDescriptionTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Personality:</div>
<div class="tokenItemizingSubclass"> {{charPersonalityTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Scenario: </div>
<div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Examples:</div>
<div class="tokenItemizingSubclass"> {{examplesStringTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- User Persona:</div>
<div class="tokenItemizingSubclass"> {{userPersonaStringTokens}}</div>
</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: gold;">World Info:</div>
<div class="">{{worldInfoStringTokens}}</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: palegreen;">Chat History:</div>
<div class=""> {{ActualChatHistoryTokens}}</div>
</div>
<div class="wide100p flex-container flexNoGap flexFlowColumn">
<div class="wide100p flex-container">
<div class="flex1" style="color: cornflowerblue;">Extensions:</div>
<div class="">{{allAnchorsTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Summarize: </div>
<div class="tokenItemizingSubclass">{{summarizeStringTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Author's Note:</div>
<div class="tokenItemizingSubclass"> {{authorsNoteStringTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Smart Context:</div>
<div class="tokenItemizingSubclass"> {{smartContextStringTokens}}</div>
</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: mediumpurple;">&lcub;&lcub;&rcub;&rcub; Bias:</div>
<div class="">{{oaiBiasTokens}}</div>
</div>
</div>
</div>
<hr>
<div class="wide100p flex-container flexFlowColumns">
<div class="flex-container wide100p">
<div class="flex1">Total Tokens in Prompt:</div>
<div class=""> {{finalPromptTokens}}</div>
</div>
<div class="flex-container wide100p">
<div class="flex1">Max Context (Context Size - Response Length):</div>
<div class="">{{thisPrompt_max_context}}</div>
</div>
</div>
</div>
<hr>

View File

@ -0,0 +1,108 @@
<h3 class="flex-container justifyCenter alignitemscenter">
Prompt Itemization
<div id="showRawPrompt" class="fa-solid fa-square-poll-horizontal menu_button"></div>
</h3>
Tokenizer: {{selectedTokenizer}}<br>
API Used: {{this_main_api}}<br>
<span class="tokenItemizingSubclass">
Only the white numbers really matter. All numbers are estimates.
Grey color items may not have been included in the context due to certain prompt format settings.
</span>
<hr>
<div class="justifyLeft">
<div class="flex-container">
<div class="flex-container flex1 flexFlowColumns flexNoGap wide50p tokenGraph">
<div class="wide100p" style="background-color: indianred; height: {{storyStringTokensPercentage}}%;"></div>
<div class="wide100p" style="background-color: gold; height: {{worldInfoStringTokensPercentage}}%;"></div>
<div class="wide100p" style="background-color: palegreen; height: {{ActualChatHistoryTokensPercentage}}%;">
</div>
<div class="wide100p" style="background-color: cornflowerblue; height: {{allAnchorsTokensPercentage}}%;">
</div>
<div class="wide100p" style="background-color: mediumpurple; height: {{promptBiasTokensPercentage}}%;">
</div>
</div>
<div class="flex-container wide50p">
<div class="wide100p flex-container flexNoGap flexFlowColumn">
<div class="flex-container wide100p">
<div class="flex1" style="color: indianred;"> Character Definitions:</div>
<div class=""> {{storyStringTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- Description: </div>
<div class="tokenItemizingSubclass">{{charDescriptionTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- Personality:</div>
<div class="tokenItemizingSubclass"> {{charPersonalityTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- Scenario: </div>
<div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- Examples:</div>
<div class="tokenItemizingSubclass"> {{examplesStringTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- User Persona:</div>
<div class="tokenItemizingSubclass"> {{userPersonaStringTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- System Prompt (Instruct):</div>
<div class="tokenItemizingSubclass"> {{instructionTokens}}</div>
</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: gold;">World Info:</div>
<div class="">{{worldInfoStringTokens}}</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: palegreen;">Chat History:</div>
<div class=""> {{ActualChatHistoryTokens}}</div>
</div>
<div class="wide100p flex-container flexNoGap flexFlowColumn">
<div class="wide100p flex-container">
<div class="flex1" style="color: cornflowerblue;">Extensions:</div>
<div class="">{{allAnchorsTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- Summarize: </div>
<div class="tokenItemizingSubclass">{{summarizeStringTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- Author's Note:</div>
<div class="tokenItemizingSubclass"> {{authorsNoteStringTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- Smart Context:</div>
<div class="tokenItemizingSubclass"> {{smartContextStringTokens}}</div>
</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: mediumpurple;">&lcub;&lcub;&rcub;&rcub; Bias:</div>
<div class="">{{promptBiasTokens}}</div>
</div>
</div>
</div>
<hr>
<div class="wide100p flex-container flexFlowColumns">
<div class="flex-container wide100p">
<div class="flex1">Total Tokens in Prompt:</div>
<div class=""> {{totalTokensInPrompt}}</div>
</div>
<div class="flex-container wide100p">
<div class="flex1">Max Context (Context Size - Response Length):</div>
<div class="">{{thisPrompt_max_context}}</div>
</div>
<div class="flex-container wide100p">
<div class="flex1">- Padding:</div>
<div class=""> {{thisPrompt_padding}}</div>
</div>
<div class="flex-container wide100p">
<div class="flex1">Actual Max Context Allowed:</div>
<div class="">{{thisPrompt_actual}}</div>
</div>
</div>
</div>
<hr>

View File

@ -0,0 +1,11 @@
System-wide Replacement Macros:
<ul>
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> - your current Persona username</li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> - the Character's name</li>
<li><tt>&lcub;&lcub;input&rcub;&rcub;</tt> - the user input</li>
<li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> - the current time</li>
<li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> - the current date</li>
<li><tt>&lcub;&lcub;idle_duration&rcub;&rcub;</tt> - the time since the last user message was sent</li>
<li><tt>&lcub;&lcub;random:(args)&rcub;&rcub;</tt> - returns a random item from the list. (ex: &lcub;&lcub;random:1,2,3,4&rcub;&rcub; will return 1 of the 4 numbers at random. Works with text lists too.</li>
<li><tt>&lcub;&lcub;roll:(formula)&rcub;&rcub;</tt> - rolls a dice. (ex: &lcub;&lcub;roll:1d6&rcub;&rcub; will roll a 6-sided dice and return a number between 1 and 6)</li>
</ul>

View File

@ -0,0 +1,72 @@
<h3>
<span id="version_display_welcome">SillyTavern</span>
<div id="version_display_welcome"></div>
</h3>
<a href="https://docs.sillytavern.app/usage/update/"" target=" _blank">
Want to update?
</a>
<hr>
<h3>How to start chatting?</h3>
<ol>
<li>Click <code><i class="fa-solid fa-plug"></i></code> and select a <a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank">Chat API</a>.</li>
<li>Click <code><i class="fa-solid fa-address-card"></i></code> and pick a character</li>
</ol>
<hr>
<h3>
Want more characters?
</h3>
<i>
Not controlled by SillyTavern team.
</i>
<ul>
<li>
<a target="_blank" href="https://discord.gg/pygmalionai">
Pygmalion AI Discord
</a>
</li>
<li>
<a target="_blank" href="https://chub.ai/">
Chub (NSFW)
</a>
</li>
</ul>
<hr>
<h3>Confused or lost?</h3>
<ul>
<li>
<span class="note-link-span">?</span> - click these icons!
</li>
<li>
Enter <code>/?</code> in the chat bar
</li>
<li>
<a target="_blank" href="https://docs.sillytavern.app/">
SillyTavern Documentation Site
</a>
</li>
<li>
<a target="_blank" href="https://docs.sillytavern.app/extras/installation/">
Extras Installation Guide
</a>
</li>
</ul>
<hr>
<h3>Still have questions?</h3>
<ul>
<li>
<a target="_blank" href="https://discord.gg/RZdyAEUPvj">
Join the SillyTavern Discord
</a>
</li>
<li>
<a target="_blank" href="https://github.com/SillyTavern/SillyTavern/issues">
Post a GitHub issue
</a>
</li>
<li>
<a target="_blank" href="https://github.com/SillyTavern/SillyTavern#questions-or-suggestions">
Contact the developers
</a>
</li>
</ul>

View File

@ -6,8 +6,6 @@ import {
setGenerationParamsFromPreset, setGenerationParamsFromPreset,
} from "../script.js"; } from "../script.js";
import { getCfg } from "./extensions/cfg/util.js";
import { import {
power_user, power_user,
} from "./power-user.js"; } from "./power-user.js";
@ -170,9 +168,9 @@ $(document).ready(function () {
textgenerationwebui_settings[id] = value; textgenerationwebui_settings[id] = value;
} }
else { else {
const value = parseFloat($(this).val()); const value = Number($(this).val());
$(`#${id}_counter_textgenerationwebui`).text(value.toFixed(2)); $(`#${id}_counter_textgenerationwebui`).text(value.toFixed(2));
textgenerationwebui_settings[id] = parseFloat(value); textgenerationwebui_settings[id] = value;
} }
saveSettingsDebounced(); saveSettingsDebounced();
@ -209,7 +207,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
const response = await fetch('/generate_textgenerationwebui', { const response = await fetch('/generate_textgenerationwebui', {
headers: { headers: {
...getRequestHeaders(), ...getRequestHeaders(),
'X-Response-Streaming': true, 'X-Response-Streaming': String(true),
'X-Streaming-URL': textgenerationwebui_settings.streaming_url, 'X-Streaming-URL': textgenerationwebui_settings.streaming_url,
}, },
body: JSON.stringify(generate_data), body: JSON.stringify(generate_data),
@ -235,9 +233,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
} }
} }
export function getTextGenGenerationData(finalPromt, this_amount_gen, isImpersonate) { export function getTextGenGenerationData(finalPromt, this_amount_gen, isImpersonate, cfgValues) {
const cfgValues = getCfg();
return { return {
'prompt': finalPromt, 'prompt': finalPromt,
'max_new_tokens': this_amount_gen, 'max_new_tokens': this_amount_gen,
@ -255,7 +251,7 @@ export function getTextGenGenerationData(finalPromt, this_amount_gen, isImperson
'penalty_alpha': textgenerationwebui_settings.penalty_alpha, 'penalty_alpha': textgenerationwebui_settings.penalty_alpha,
'length_penalty': textgenerationwebui_settings.length_penalty, 'length_penalty': textgenerationwebui_settings.length_penalty,
'early_stopping': textgenerationwebui_settings.early_stopping, 'early_stopping': textgenerationwebui_settings.early_stopping,
'guidance_scale': cfgValues?.guidanceScale ?? textgenerationwebui_settings.guidance_scale ?? 1, 'guidance_scale': cfgValues?.guidanceScale?.value ?? textgenerationwebui_settings.guidance_scale ?? 1,
'negative_prompt': cfgValues?.negativePrompt ?? textgenerationwebui_settings.negative_prompt ?? '', 'negative_prompt': cfgValues?.negativePrompt ?? textgenerationwebui_settings.negative_prompt ?? '',
'seed': textgenerationwebui_settings.seed, 'seed': textgenerationwebui_settings.seed,
'add_bos_token': textgenerationwebui_settings.add_bos_token, 'add_bos_token': textgenerationwebui_settings.add_bos_token,

View File

@ -0,0 +1,342 @@
import { characters, main_api, nai_settings, this_chid } from "../script.js";
import { power_user } from "./power-user.js";
import { encode } from "../lib/gpt-2-3-tokenizer/mod.js";
import { GPT3BrowserTokenizer } from "../lib/gpt-3-tokenizer/gpt3-tokenizer.js";
import { chat_completion_sources, oai_settings } from "./openai.js";
import { groups, selected_group } from "./group-chats.js";
import { getStringHash } from "./utils.js";
export const CHARACTERS_PER_TOKEN_RATIO = 3.35;
export const tokenizers = {
NONE: 0,
GPT3: 1,
CLASSIC: 2,
LLAMA: 3,
NERD: 4,
NERD2: 5,
API: 6,
BEST_MATCH: 99,
};
const objectStore = new localforage.createInstance({ name: "SillyTavern_ChatCompletions" });
const gpt3 = new GPT3BrowserTokenizer({ type: 'gpt3' });
let tokenCache = {};
async function loadTokenCache() {
try {
console.debug('Chat Completions: loading token cache')
tokenCache = await objectStore.getItem('tokenCache') || {};
} catch (e) {
console.log('Chat Completions: unable to load token cache, using default value', e);
tokenCache = {};
}
}
export async function saveTokenCache() {
try {
console.debug('Chat Completions: saving token cache')
await objectStore.setItem('tokenCache', tokenCache);
} catch (e) {
console.log('Chat Completions: unable to save token cache', e);
}
}
async function resetTokenCache() {
try {
console.debug('Chat Completions: resetting token cache');
Object.keys(tokenCache).forEach(key => delete tokenCache[key]);
await objectStore.removeItem('tokenCache');
} catch (e) {
console.log('Chat Completions: unable to reset token cache', e);
}
}
window['resetTokenCache'] = resetTokenCache;
function getTokenizerBestMatch() {
if (main_api === 'novel') {
if (nai_settings.model_novel.includes('krake') || nai_settings.model_novel.includes('euterpe')) {
return tokenizers.CLASSIC;
}
if (nai_settings.model_novel.includes('clio')) {
return tokenizers.NERD;
}
if (nai_settings.model_novel.includes('kayra')) {
return tokenizers.NERD2;
}
}
if (main_api === 'kobold' || main_api === 'textgenerationwebui' || main_api === 'koboldhorde') {
return tokenizers.LLAMA;
}
return tokenizers.NONE;
}
/**
* Gets the token count for a string using the current model tokenizer.
* @param {string} str String to tokenize
* @param {number | undefined} padding Optional padding tokens. Defaults to 0.
* @returns {number} Token count.
*/
export function getTokenCount(str, padding = undefined) {
/**
* Calculates the token count for a string.
* @param {number} [type] Tokenizer type.
* @returns {number} Token count.
*/
function calculate(type) {
switch (type) {
case tokenizers.NONE:
return Math.ceil(str.length / CHARACTERS_PER_TOKEN_RATIO) + padding;
case tokenizers.GPT3:
return gpt3.encode(str).bpe.length + padding;
case tokenizers.CLASSIC:
return encode(str).length + padding;
case tokenizers.LLAMA:
return countTokensRemote('/tokenize_llama', str, padding);
case tokenizers.NERD:
return countTokensRemote('/tokenize_nerdstash', str, padding);
case tokenizers.NERD2:
return countTokensRemote('/tokenize_nerdstash_v2', str, padding);
case tokenizers.API:
return countTokensRemote('/tokenize_via_api', str, padding);
default:
console.warn("Unknown tokenizer type", type);
return calculate(tokenizers.NONE);
}
}
if (typeof str !== 'string' || !str?.length) {
return 0;
}
let tokenizerType = power_user.tokenizer;
if (main_api === 'openai') {
if (padding === power_user.token_padding) {
// For main "shadow" prompt building
tokenizerType = tokenizers.NONE;
} else {
// For extensions and WI
return counterWrapperOpenAI(str);
}
}
if (tokenizerType === tokenizers.BEST_MATCH) {
tokenizerType = getTokenizerBestMatch();
}
if (padding === undefined) {
padding = 0;
}
const cacheObject = getTokenCacheObject();
const hash = getStringHash(str);
const cacheKey = `${tokenizerType}-${hash}`;
if (typeof cacheObject[cacheKey] === 'number') {
return cacheObject[cacheKey];
}
const result = calculate(tokenizerType);
if (isNaN(result)) {
console.warn("Token count calculation returned NaN");
return 0;
}
cacheObject[cacheKey] = result;
return result;
}
/**
* Gets the token count for a string using the OpenAI tokenizer.
* @param {string} text Text to tokenize.
* @returns {number} Token count.
*/
function counterWrapperOpenAI(text) {
const message = { role: 'system', content: text };
return countTokensOpenAI(message, true);
}
export function getTokenizerModel() {
// OpenAI models always provide their own tokenizer
if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
return oai_settings.openai_model;
}
const turboTokenizer = 'gpt-3.5-turbo';
const gpt4Tokenizer = 'gpt-4';
const gpt2Tokenizer = 'gpt2';
const claudeTokenizer = 'claude';
// Assuming no one would use it for different models.. right?
if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
return gpt4Tokenizer;
}
// Select correct tokenizer for WindowAI proxies
if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI && oai_settings.windowai_model) {
if (oai_settings.windowai_model.includes('gpt-4')) {
return gpt4Tokenizer;
}
else if (oai_settings.windowai_model.includes('gpt-3.5-turbo')) {
return turboTokenizer;
}
else if (oai_settings.windowai_model.includes('claude')) {
return claudeTokenizer;
}
else if (oai_settings.windowai_model.includes('GPT-NeoXT')) {
return gpt2Tokenizer;
}
}
// And for OpenRouter (if not a site model, then it's impossible to determine the tokenizer)
if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model) {
if (oai_settings.openrouter_model.includes('gpt-4')) {
return gpt4Tokenizer;
}
else if (oai_settings.openrouter_model.includes('gpt-3.5-turbo')) {
return turboTokenizer;
}
else if (oai_settings.openrouter_model.includes('claude')) {
return claudeTokenizer;
}
else if (oai_settings.openrouter_model.includes('GPT-NeoXT')) {
return gpt2Tokenizer;
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
return claudeTokenizer;
}
// Default to Turbo 3.5
return turboTokenizer;
}
/**
* @param {any[] | Object} messages
*/
export function countTokensOpenAI(messages, full = false) {
const shouldTokenizeAI21 = oai_settings.chat_completion_source === chat_completion_sources.AI21 && oai_settings.use_ai21_tokenizer;
const cacheObject = getTokenCacheObject();
if (!Array.isArray(messages)) {
messages = [messages];
}
let token_count = -1;
for (const message of messages) {
const model = getTokenizerModel();
if (model === 'claude' || shouldTokenizeAI21) {
full = true;
}
const hash = getStringHash(JSON.stringify(message));
const cacheKey = `${model}-${hash}`;
const cachedCount = cacheObject[cacheKey];
if (typeof cachedCount === 'number') {
token_count += cachedCount;
}
else {
jQuery.ajax({
async: false,
type: 'POST', //
url: shouldTokenizeAI21 ? '/tokenize_ai21' : `/tokenize_openai?model=${model}`,
data: JSON.stringify([message]),
dataType: "json",
contentType: "application/json",
success: function (data) {
token_count += Number(data.token_count);
cacheObject[cacheKey] = Number(data.token_count);
}
});
}
}
if (!full) token_count -= 2;
return token_count;
}
/**
* Gets the token cache object for the current chat.
* @returns {Object} Token cache object for the current chat.
*/
function getTokenCacheObject() {
let chatId = 'undefined';
try {
if (selected_group) {
chatId = groups.find(x => x.id == selected_group)?.chat_id;
}
else if (this_chid !== undefined) {
chatId = characters[this_chid].chat;
}
} catch {
console.log('No character / group selected. Using default cache item');
}
if (typeof tokenCache[chatId] !== 'object') {
tokenCache[chatId] = {};
}
return tokenCache[String(chatId)];
}
function countTokensRemote(endpoint, str, padding) {
let tokenCount = 0;
jQuery.ajax({
async: false,
type: 'POST',
url: endpoint,
data: JSON.stringify({ text: str }),
dataType: "json",
contentType: "application/json",
success: function (data) {
tokenCount = data.count;
}
});
return tokenCount + padding;
}
function getTextTokensRemote(endpoint, str) {
let ids = [];
jQuery.ajax({
async: false,
type: 'POST',
url: endpoint,
data: JSON.stringify({ text: str }),
dataType: "json",
contentType: "application/json",
success: function (data) {
ids = data.ids;
}
});
return ids;
}
export function getTextTokens(tokenizerType, str) {
switch (tokenizerType) {
case tokenizers.LLAMA:
return getTextTokensRemote('/tokenize_llama', str);
case tokenizers.NERD:
return getTextTokensRemote('/tokenize_nerdstash', str);
case tokenizers.NERD2:
return getTextTokensRemote('/tokenize_nerdstash_v2', str);
default:
console.warn("Calling getTextTokens with unsupported tokenizer type", tokenizerType);
return [];
}
}
jQuery(async () => {
await loadTokenCache();
});

View File

@ -1,21 +1,56 @@
import { getContext } from "./extensions.js"; import { getContext } from "./extensions.js";
import { getRequestHeaders } from "../script.js"; import { getRequestHeaders } from "../script.js";
/**
* Pagination status string template.
* @type {string}
*/
export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>'; export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>';
/**
* Navigation options for pagination.
* @enum {number}
*/
export const navigation_option = { none: 0, previous: 1, last: 2, };
/**
* Determines if a value is unique in an array.
* @param {any} value Current value.
* @param {number} index Current index.
* @param {any} array The array being processed.
* @returns {boolean} True if the value is unique, false otherwise.
*/
export function onlyUnique(value, index, array) { export function onlyUnique(value, index, array) {
return array.indexOf(value) === index; return array.indexOf(value) === index;
} }
/**
* Checks if a string only contains digits.
* @param {string} str The string to check.
* @returns {boolean} True if the string only contains digits, false otherwise.
* @example
* isDigitsOnly('123'); // true
* isDigitsOnly('abc'); // false
*/
export function isDigitsOnly(str) { export function isDigitsOnly(str) {
return /^\d+$/.test(str); return /^\d+$/.test(str);
} }
// Increase delay on touch screens /**
* Gets a drag delay for sortable elements. This is to prevent accidental drags when scrolling.
* @returns {number} The delay in milliseconds. 100ms for desktop, 750ms for mobile.
*/
export function getSortableDelay() { export function getSortableDelay() {
return navigator.maxTouchPoints > 0 ? 750 : 100; return navigator.maxTouchPoints > 0 ? 750 : 100;
} }
/**
* Rearranges an array in a random order.
* @param {any[]} array The array to shuffle.
* @returns {any[]} The shuffled array.
* @example
* shuffle([1, 2, 3]); // [2, 3, 1]
*/
export function shuffle(array) { export function shuffle(array) {
let currentIndex = array.length, let currentIndex = array.length,
randomIndex; randomIndex;
@ -31,6 +66,12 @@ export function shuffle(array) {
return array; return array;
} }
/**
* Downloads a file to the user's devices.
* @param {BlobPart} content File content to download.
* @param {string} fileName File name.
* @param {string} contentType File content type.
*/
export function download(content, fileName, contentType) { export function download(content, fileName, contentType) {
const a = document.createElement("a"); const a = document.createElement("a");
const file = new Blob([content], { type: contentType }); const file = new Blob([content], { type: contentType });
@ -39,22 +80,38 @@ export function download(content, fileName, contentType) {
a.click(); a.click();
} }
/**
* Fetches a file by URL and parses its contents as data URI.
* @param {string} url The URL to fetch.
* @param {any} params Fetch parameters.
* @returns {Promise<string>} A promise that resolves to the data URI.
*/
export async function urlContentToDataUri(url, params) { export async function urlContentToDataUri(url, params) {
const response = await fetch(url, params); const response = await fetch(url, params);
const blob = await response.blob(); const blob = await response.blob();
return await new Promise(callback => { return await new Promise((resolve, reject) => {
let reader = new FileReader(); const reader = new FileReader();
reader.onload = function () { callback(this.result); }; reader.onload = function () {
resolve(String(reader.result));
};
reader.onerror = function (error) {
reject(error);
};
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
} }
/**
* Returns a promise that resolves to the file's text.
* @param {Blob} file The file to read.
* @returns {Promise<string>} A promise that resolves to the file's text.
*/
export function getFileText(file) { export function getFileText(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsText(file); reader.readAsText(file);
reader.onload = function () { reader.onload = function () {
resolve(reader.result); resolve(String(reader.result));
}; };
reader.onerror = function (error) { reader.onerror = function (error) {
reject(error); reject(error);
@ -62,6 +119,10 @@ export function getFileText(file) {
}); });
} }
/**
* Returns a promise that resolves to the file's array buffer.
* @param {Blob} file The file to read.
*/
export function getFileBuffer(file) { export function getFileBuffer(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
@ -75,12 +136,17 @@ export function getFileBuffer(file) {
}); });
} }
/**
* Returns a promise that resolves to the base64 encoded string of a file.
* @param {Blob} file The file to read.
* @returns {Promise<string>} A promise that resolves to the base64 encoded string.
*/
export function getBase64Async(file) { export function getBase64Async(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsDataURL(file); reader.readAsDataURL(file);
reader.onload = function () { reader.onload = function () {
resolve(reader.result); resolve(String(reader.result));
}; };
reader.onerror = function (error) { reader.onerror = function (error) {
reject(error); reject(error);
@ -88,15 +154,26 @@ export function getBase64Async(file) {
}); });
} }
/**
* Parses a file blob as a JSON object.
* @param {Blob} file The file to read.
* @returns {Promise<any>} A promise that resolves to the parsed JSON object.
*/
export async function parseJsonFile(file) { export async function parseJsonFile(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = event => resolve(JSON.parse(event.target.result));
fileReader.onerror = error => reject(error);
fileReader.readAsText(file); fileReader.readAsText(file);
fileReader.onload = event => resolve(JSON.parse(String(event.target.result)));
fileReader.onerror = error => reject(error);
}); });
} }
/**
* Calculates a hash code for a string.
* @param {string} str The string to hash.
* @param {number} [seed=0] The seed to use for the hash.
* @returns {number} The hash code.
*/
export function getStringHash(str, seed = 0) { export function getStringHash(str, seed = 0) {
if (typeof str !== 'string') { if (typeof str !== 'string') {
return 0; return 0;
@ -116,6 +193,12 @@ export function getStringHash(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1 >>> 0); return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}; };
/**
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
* @param {function} func The function to debounce.
* @param {number} [timeout=300] The timeout in milliseconds.
* @returns {function} The debounced function.
*/
export function debounce(func, timeout = 300) { export function debounce(func, timeout = 300) {
let timer; let timer;
return (...args) => { return (...args) => {
@ -124,6 +207,12 @@ export function debounce(func, timeout = 300) {
}; };
} }
/**
* Creates a throttled function that only invokes func at most once per every limit milliseconds.
* @param {function} func The function to throttle.
* @param {number} [limit=300] The limit in milliseconds.
* @returns {function} The throttled function.
*/
export function throttle(func, limit = 300) { export function throttle(func, limit = 300) {
let lastCall; let lastCall;
return (...args) => { return (...args) => {
@ -135,6 +224,11 @@ export function throttle(func, limit = 300) {
}; };
} }
/**
* Checks if an element is in the viewport.
* @param {Element} el The element to check.
* @returns {boolean} True if the element is in the viewport, false otherwise.
*/
export function isElementInViewport(el) { export function isElementInViewport(el) {
if (typeof jQuery === "function" && el instanceof jQuery) { if (typeof jQuery === "function" && el instanceof jQuery) {
el = el[0]; el = el[0];
@ -148,6 +242,12 @@ export function isElementInViewport(el) {
); );
} }
/**
* Returns a name that is unique among the names that exist.
* @param {string} name The name to check.
* @param {{ (y: any): boolean; }} exists Function to check if name exists.
* @returns {string} A unique name.
*/
export function getUniqueName(name, exists) { export function getUniqueName(name, exists) {
let i = 1; let i = 1;
let baseName = name; let baseName = name;
@ -158,18 +258,48 @@ export function getUniqueName(name, exists) {
return name; return name;
} }
export const delay = (ms) => new Promise((res) => setTimeout(res, ms)); /**
export const isSubsetOf = (a, b) => (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false; * Returns a promise that resolves after the specified number of milliseconds.
* @param {number} ms The number of milliseconds to wait.
* @returns {Promise<void>} A promise that resolves after the specified number of milliseconds.
*/
export function delay(ms) {
return new Promise((res) => setTimeout(res, ms));
}
/**
* Checks if an array is a subset of another array.
* @param {any[]} a Array A
* @param {any[]} b Array B
* @returns {boolean} True if B is a subset of A, false otherwise.
*/
export function isSubsetOf(a, b) {
return (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false;
}
/**
* Increments the trailing number in a string.
* @param {string} str The string to process.
* @returns {string} The string with the trailing number incremented by 1.
* @example
* incrementString('Hello, world! 1'); // 'Hello, world! 2'
*/
export function incrementString(str) { export function incrementString(str) {
// Find the trailing number or it will match the empty string // Find the trailing number or it will match the empty string
const count = str.match(/\d*$/); const count = str.match(/\d*$/);
// Take the substring up until where the integer was matched // Take the substring up until where the integer was matched
// Concatenate it to the matched count incremented by 1 // Concatenate it to the matched count incremented by 1
return str.substr(0, count.index) + (++count[0]); return str.substring(0, count.index) + (Number(count[0]) + 1);
}; };
/**
* Formats a string using the specified arguments.
* @param {string} format The format string.
* @returns {string} The formatted string.
* @example
* stringFormat('Hello, {0}!', 'world'); // 'Hello, world!'
*/
export function stringFormat(format) { export function stringFormat(format) {
const args = Array.prototype.slice.call(arguments, 1); const args = Array.prototype.slice.call(arguments, 1);
return format.replace(/{(\d+)}/g, function (match, number) { return format.replace(/{(\d+)}/g, function (match, number) {
@ -180,7 +310,11 @@ export function stringFormat(format) {
}); });
}; };
// Save the caret position in a contenteditable element /**
* Save the caret position in a contenteditable element.
* @param {Element} element The element to save the caret position of.
* @returns {{ start: number, end: number }} An object with the start and end offsets of the caret.
*/
export function saveCaretPosition(element) { export function saveCaretPosition(element) {
// Get the current selection // Get the current selection
const selection = window.getSelection(); const selection = window.getSelection();
@ -209,7 +343,11 @@ export function saveCaretPosition(element) {
return position; return position;
} }
// Restore the caret position in a contenteditable element /**
* Restore the caret position in a contenteditable element.
* @param {Element} element The element to restore the caret position of.
* @param {{ start: any; end: any; }} position An object with the start and end offsets of the caret.
*/
export function restoreCaretPosition(element, position) { export function restoreCaretPosition(element, position) {
// If the position is null, do nothing // If the position is null, do nothing
if (!position) { if (!position) {
@ -236,6 +374,11 @@ export async function resetScrollHeight(element) {
$(element).css('height', $(element).prop('scrollHeight') + 3 + 'px'); $(element).css('height', $(element).prop('scrollHeight') + 3 + 'px');
} }
/**
* Sets the height of an element to its scroll height.
* @param {JQuery<HTMLElement>} element The element to initialize the scroll height of.
* @returns {Promise<void>} A promise that resolves when the scroll height has been initialized.
*/
export async function initScrollHeight(element) { export async function initScrollHeight(element) {
await delay(1); await delay(1);
@ -252,15 +395,27 @@ export async function initScrollHeight(element) {
//resetScrollHeight(element); //resetScrollHeight(element);
} }
/**
* Compares elements by their CSS order property. Used for sorting.
* @param {any} a The first element.
* @param {any} b The second element.
* @returns {number} A negative number if a is before b, a positive number if a is after b, or 0 if they are equal.
*/
export function sortByCssOrder(a, b) { export function sortByCssOrder(a, b) {
const _a = Number($(a).css('order')); const _a = Number($(a).css('order'));
const _b = Number($(b).css('order')); const _b = Number($(b).css('order'));
return _a - _b; return _a - _b;
} }
/**
* Trims a string to the end of a nearest sentence.
* @param {string} input The string to trim.
* @param {boolean} include_newline Whether to include a newline character in the trimmed string.
* @returns {string} The trimmed string.
* @example
* end_trim_to_sentence('Hello, world! I am from'); // 'Hello, world!'
*/
export function end_trim_to_sentence(input, include_newline = false) { export function end_trim_to_sentence(input, include_newline = false) {
// inspired from https://github.com/kaihordewebui/kaihordewebui.github.io/blob/06b95e6b7720eb85177fbaf1a7f52955d7cdbc02/index.html#L4853-L4867
const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '', '', '”', '', '】', '】', '', '」', '】']); // extend this as you see fit const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '', '', '”', '', '】', '】', '', '」', '】']); // extend this as you see fit
let last = -1; let last = -1;
@ -285,6 +440,15 @@ export function end_trim_to_sentence(input, include_newline = false) {
return input.substring(0, last + 1).trimEnd(); return input.substring(0, last + 1).trimEnd();
} }
/**
* Counts the number of occurrences of a character in a string.
* @param {string} string The string to count occurrences in.
* @param {string} character The character to count occurrences of.
* @returns {number} The number of occurrences of the character in the string.
* @example
* countOccurrences('Hello, world!', 'l'); // 3
* countOccurrences('Hello, world!', 'x'); // 0
*/
export function countOccurrences(string, character) { export function countOccurrences(string, character) {
let count = 0; let count = 0;
@ -297,6 +461,14 @@ export function countOccurrences(string, character) {
return count; return count;
} }
/**
* Checks if a number is odd.
* @param {number} number The number to check.
* @returns {boolean} True if the number is odd, false otherwise.
* @example
* isOdd(3); // true
* isOdd(4); // false
*/
export function isOdd(number) { export function isOdd(number) {
return number % 2 !== 0; return number % 2 !== 0;
} }
@ -337,6 +509,12 @@ export function timestampToMoment(timestamp) {
return moment.invalid(); return moment.invalid();
} }
/**
* Compare two moment objects for sorting.
* @param {*} a The first moment object.
* @param {*} b The second moment object.
* @returns {number} A negative number if a is before b, a positive number if a is after b, or 0 if they are equal.
*/
export function sortMoments(a, b) { export function sortMoments(a, b) {
if (a.isBefore(b)) { if (a.isBefore(b)) {
return 1; return 1;
@ -347,14 +525,21 @@ export function sortMoments(a, b) {
} }
} }
/** Split string to parts no more than length in size */ /** Split string to parts no more than length in size.
export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ', '']) { * @param {string} input The string to split.
const delim = delimitiers[0] ?? ''; * @param {number} length The maximum length of each part.
* @param {string[]} delimiters The delimiters to use when splitting the string.
* @returns {string[]} The split string.
* @example
* splitRecursive('Hello, world!', 3); // ['Hel', 'lo,', 'wor', 'ld!']
*/
export function splitRecursive(input, length, delimiters = ['\n\n', '\n', ' ', '']) {
const delim = delimiters[0] ?? '';
const parts = input.split(delim); const parts = input.split(delim);
const flatParts = parts.flatMap(p => { const flatParts = parts.flatMap(p => {
if (p.length < length) return p; if (p.length < length) return p;
return splitRecursive(input, length, delimitiers.slice(1)); return splitRecursive(input, length, delimiters.slice(1));
}); });
// Merge short chunks // Merge short chunks
@ -378,6 +563,13 @@ export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ',
return result; return result;
} }
/**
* Checks if a string is a valid data URL.
* @param {string} str The string to check.
* @returns {boolean} True if the string is a valid data URL, false otherwise.
* @example
* isDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...'); // true
*/
export function isDataURL(str) { export function isDataURL(str) {
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i; const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i;
return regex.test(str); return regex.test(str);
@ -392,6 +584,13 @@ export function getCharaFilename(chid) {
} }
} }
/**
* Extracts words from a string.
* @param {string} value The string to extract words from.
* @returns {string[]} The extracted words.
* @example
* extractAllWords('Hello, world!'); // ['hello', 'world']
*/
export function extractAllWords(value) { export function extractAllWords(value) {
const words = []; const words = [];
@ -406,21 +605,45 @@ export function extractAllWords(value) {
return words; return words;
} }
/**
* Escapes a string for use in a regular expression.
* @param {string} string The string to escape.
* @returns {string} The escaped string.
* @example
* escapeRegex('^Hello$'); // '\\^Hello\\$'
*/
export function escapeRegex(string) { export function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
} }
/**
* Provides an interface for rate limiting function calls.
*/
export class RateLimiter { export class RateLimiter {
constructor(intervalMillis) { /**
this._intervalMillis = intervalMillis; * Creates a new RateLimiter.
this._lastResolveTime = 0; * @param {number} interval The interval in milliseconds.
this._pendingResolve = Promise.resolve(); * @example
* const rateLimiter = new RateLimiter(1000);
* rateLimiter.waitForResolve().then(() => {
* console.log('Waited 1000ms');
* });
*/
constructor(interval) {
this.interval = interval;
this.lastResolveTime = 0;
this.pendingResolve = Promise.resolve();
} }
/**
* Waits for the remaining time in the interval.
* @param {AbortSignal} abortSignal An optional AbortSignal to abort the wait.
* @returns {Promise<void>} A promise that resolves when the remaining time has elapsed.
*/
_waitRemainingTime(abortSignal) { _waitRemainingTime(abortSignal) {
const currentTime = Date.now(); const currentTime = Date.now();
const elapsedTime = currentTime - this._lastResolveTime; const elapsedTime = currentTime - this.lastResolveTime;
const remainingTime = Math.max(0, this._intervalMillis - elapsedTime); const remainingTime = Math.max(0, this.interval - elapsedTime);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@ -436,19 +659,29 @@ export class RateLimiter {
}); });
} }
/**
* Waits for the next interval to elapse.
* @param {AbortSignal} abortSignal An optional AbortSignal to abort the wait.
* @returns {Promise<void>} A promise that resolves when the next interval has elapsed.
*/
async waitForResolve(abortSignal) { async waitForResolve(abortSignal) {
await this._pendingResolve; await this.pendingResolve;
this._pendingResolve = this._waitRemainingTime(abortSignal); this.pendingResolve = this._waitRemainingTime(abortSignal);
// Update the last resolve time // Update the last resolve time
this._lastResolveTime = Date.now() + this._intervalMillis; this.lastResolveTime = Date.now() + this.interval;
console.debug(`RateLimiter.waitForResolve() ${this._lastResolveTime}`); console.debug(`RateLimiter.waitForResolve() ${this.lastResolveTime}`);
} }
} }
// Taken from https://github.com/LostRuins/lite.koboldai.net/blob/main/index.html /**
//import tavern png data. adapted from png-chunks-extract under MIT license * Extracts a JSON object from a PNG file.
//accepts png input data, and returns the extracted JSON * Taken from https://github.com/LostRuins/lite.koboldai.net/blob/main/index.html
* Adapted from png-chunks-extract under MIT license
* @param {Uint8Array} data The PNG data to extract the JSON from.
* @param {string} identifier The identifier to look for in the PNG tEXT data.
* @returns {object} The extracted JSON object.
*/
export function extractDataFromPng(data, identifier = 'chara') { export function extractDataFromPng(data, identifier = 'chara') {
console.log("Attempting PNG import..."); console.log("Attempting PNG import...");
let uint8 = new Uint8Array(4); let uint8 = new Uint8Array(4);
@ -599,6 +832,13 @@ export async function saveBase64AsFile(base64Data, characterName, filename = "",
} }
} }
/**
* Creates a thumbnail from a data URL.
* @param {string} dataUrl The data URL encoded data of the image.
* @param {number} maxWidth The maximum width of the thumbnail.
* @param {number} maxHeight The maximum height of the thumbnail.
* @returns {Promise<string>} A promise that resolves to the thumbnail data URL.
*/
export function createThumbnail(dataUrl, maxWidth, maxHeight) { export function createThumbnail(dataUrl, maxWidth, maxHeight) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
@ -634,6 +874,13 @@ export function createThumbnail(dataUrl, maxWidth, maxHeight) {
}); });
} }
/**
* Waits for a condition to be true. Throws an error if the condition is not true within the timeout.
* @param {{ (): boolean; }} condition The condition to wait for.
* @param {number} [timeout=1000] The timeout in milliseconds.
* @param {number} [interval=100] The interval in milliseconds.
* @returns {Promise<void>} A promise that resolves when the condition is true.
*/
export async function waitUntilCondition(condition, timeout = 1000, interval = 100) { export async function waitUntilCondition(condition, timeout = 1000, interval = 100) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@ -651,6 +898,12 @@ export async function waitUntilCondition(condition, timeout = 1000, interval = 1
}); });
} }
/**
* Returns a UUID v4 string.
* @returns {string} A UUID v4 string.
* @example
* uuidv4(); // '3e2fd9e1-0a7a-4f6d-9aaf-8a7a4babe7eb'
*/
export function uuidv4() { export function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0; const r = Math.random() * 16 | 0;
@ -659,6 +912,11 @@ export function uuidv4() {
}); });
} }
/**
* Clones an object using JSON serialization.
* @param {any} obj The object to clone.
* @returns {any} A deep clone of the object.
*/
export function deepClone(obj) { export function deepClone(obj) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }

View File

@ -1,10 +1,11 @@
import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types } from "../script.js"; import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types } from "../script.js";
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, deepClone, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE } from "./utils.js"; import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, deepClone, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option } from "./utils.js";
import { getContext } from "./extensions.js"; import { getContext } from "./extensions.js";
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js"; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js";
import { registerSlashCommand } from "./slash-commands.js"; import { registerSlashCommand } from "./slash-commands.js";
import { deviceInfo } from "./RossAscends-mods.js"; import { deviceInfo } from "./RossAscends-mods.js";
import { FILTER_TYPES, FilterHelper } from "./filters.js"; import { FILTER_TYPES, FilterHelper } from "./filters.js";
import { getTokenCount } from "./tokenizers.js";
export { export {
world_info, world_info,
@ -46,7 +47,6 @@ const saveSettingsDebounced = debounce(() => {
saveSettings() saveSettings()
}, 1000); }, 1000);
const sortFn = (a, b) => b.order - a.order; const sortFn = (a, b) => b.order - a.order;
const navigation_option = { none: 0, previous: 1, last: 2, };
let updateEditor = (navigation) => { navigation; }; let updateEditor = (navigation) => { navigation; };
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data. // Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
@ -418,7 +418,7 @@ function getWorldEntry(name, data, entry) {
keyInput.on("input", function () { keyInput.on("input", function () {
const uid = $(this).data("uid"); const uid = $(this).data("uid");
const value = $(this).val(); const value = String($(this).val());
resetScrollHeight(this); resetScrollHeight(this);
data.entries[uid].key = value data.entries[uid].key = value
.split(",") .split(",")
@ -454,7 +454,7 @@ function getWorldEntry(name, data, entry) {
keySecondaryInput.data("uid", entry.uid); keySecondaryInput.data("uid", entry.uid);
keySecondaryInput.on("input", function () { keySecondaryInput.on("input", function () {
const uid = $(this).data("uid"); const uid = $(this).data("uid");
const value = $(this).val(); const value = String($(this).val());
resetScrollHeight(this); resetScrollHeight(this);
data.entries[uid].keysecondary = value data.entries[uid].keysecondary = value
.split(",") .split(",")
@ -1506,19 +1506,6 @@ jQuery(() => {
return; return;
} }
/*
if (deviceInfo.device.type === 'desktop') {
let selectScrollTop = null;
e.preventDefault();
const option = $(e.target);
const selectElement = $(this)[0];
selectScrollTop = selectElement.scrollTop;
option.prop('selected', !option.prop('selected'));
await delay(1);
selectElement.scrollTop = selectScrollTop;
}
*/
onWorldInfoChange('__notSlashCommand__'); onWorldInfoChange('__notSlashCommand__');
}); });

View File

@ -3302,6 +3302,72 @@ async function sendScaleRequest(request, response) {
} }
} }
app.post("/generate_altscale", jsonParser, function (request, response_generate_scale) {
if (!request.body) return response_generate_scale.sendStatus(400);
fetch('https://dashboard.scale.com/spellbook/api/trpc/v2.variant.run', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'cookie': `_jwt=${readSecret(SECRET_KEYS.SCALE_COOKIE)}`,
},
body: JSON.stringify({
json: {
variant: {
name: 'New Variant',
appId: '',
taxonomy: null
},
prompt: {
id: '',
template: '{{input}}\n',
exampleVariables: {},
variablesSourceDataId: null,
systemMessage: request.body.sysprompt
},
modelParameters: {
id: '',
modelId: 'GPT4',
modelType: 'OpenAi',
maxTokens: request.body.max_tokens,
temperature: request.body.temp,
stop: "user:",
suffix: null,
topP: request.body.top_p,
logprobs: null,
logitBias: request.body.logit_bias
},
inputs: [
{
index: '-1',
valueByName: {
input: request.body.prompt
}
}
]
},
meta: {
values: {
'variant.taxonomy': ['undefined'],
'prompt.variablesSourceDataId': ['undefined'],
'modelParameters.suffix': ['undefined'],
'modelParameters.logprobs': ['undefined'],
}
}
})
})
.then(response => response.json())
.then(data => {
console.log(data.result.data.json.outputs[0])
return response_generate_scale.send({ output: data.result.data.json.outputs[0] });
})
.catch((error) => {
console.error('Error:', error)
return response_generate_scale.send({ error: true })
});
});
async function sendClaudeRequest(request, response) { async function sendClaudeRequest(request, response) {
const fetch = require('node-fetch').default; const fetch = require('node-fetch').default;
@ -3990,7 +4056,8 @@ const SECRET_KEYS = {
DEEPL: 'deepl', DEEPL: 'deepl',
OPENROUTER: 'api_key_openrouter', OPENROUTER: 'api_key_openrouter',
SCALE: 'api_key_scale', SCALE: 'api_key_scale',
AI21: 'api_key_ai21' AI21: 'api_key_ai21',
SCALE_COOKIE: 'scale_cookie',
} }
function migrateSecrets() { function migrateSecrets() {