Merge branch 'staging' into feat/xtc

d77363cd7cd5410adb6b94511f725f2ec298e8e7

Cohee <18619528+Cohee1207@users.noreply.github.com>

22 files changed, +477 -98Ignore whitespace
index.d.ts+5 -0
@@ -9,6 +9,11 @@ declare global {
9 };9 };
10 }10 }
11 }11 }
12
13 /**
14 * The root directory for user data.
15 */
16 var DATA_ROOT: string;
12}17}
1318
14declare module 'express-session' {19declare module 'express-session' {
package-lock.json+30 -5
@@ -27,6 +27,7 @@
27 "google-translate-api-browser": "^3.0.1",27 "google-translate-api-browser": "^3.0.1",
28 "he": "^1.2.0",28 "he": "^1.2.0",
29 "helmet": "^7.1.0",29 "helmet": "^7.1.0",
30 "iconv-lite": "^0.6.3",
30 "ip-matching": "^2.1.2",31 "ip-matching": "^2.1.2",
31 "ipaddr.js": "^2.0.1",32 "ipaddr.js": "^2.0.1",
32 "jimp": "^0.22.10",33 "jimp": "^0.22.10",
@@ -42,7 +43,7 @@
42 "rate-limiter-flexible": "^5.0.0",43 "rate-limiter-flexible": "^5.0.0",
43 "response-time": "^2.3.2",44 "response-time": "^2.3.2",
44 "sanitize-filename": "^1.6.3",45 "sanitize-filename": "^1.6.3",
45 "sillytavern-transformers": "^2.14.6",46 "sillytavern-transformers": "2.14.6",
46 "simple-git": "^3.19.1",47 "simple-git": "^3.19.1",
47 "tiktoken": "^1.0.15",48 "tiktoken": "^1.0.15",
48 "vectra": "^0.2.2",49 "vectra": "^0.2.2",
@@ -1492,6 +1493,18 @@
1492 "node": ">= 0.8"1493 "node": ">= 0.8"
1493 }1494 }
1494 },1495 },
1496 "node_modules/body-parser/node_modules/iconv-lite": {
1497 "version": "0.4.24",
1498 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1499 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1500 "license": "MIT",
1501 "dependencies": {
1502 "safer-buffer": ">= 2.1.2 < 3"
1503 },
1504 "engines": {
1505 "node": ">=0.10.0"
1506 }
1507 },
1495 "node_modules/boolbase": {1508 "node_modules/boolbase": {
1496 "version": "1.0.0",1509 "version": "1.0.0",
1497 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",1510 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -3282,12 +3295,12 @@
3282 }3295 }
3283 },3296 },
3284 "node_modules/iconv-lite": {3297 "node_modules/iconv-lite": {
3285 "version": "0.4.24",3298 "version": "0.6.3",
3286 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",3299 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
3287 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",3300 "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
3288 "license": "MIT",3301 "license": "MIT",
3289 "dependencies": {3302 "dependencies": {
3290 "safer-buffer": ">= 2.1.2 < 3"3303 "safer-buffer": ">= 2.1.2 < 3.0.0"
3291 },3304 },
3292 "engines": {3305 "engines": {
3293 "node": ">=0.10.0"3306 "node": ">=0.10.0"
@@ -4618,6 +4631,18 @@
4618 "node": ">= 0.8"4631 "node": ">= 0.8"
4619 }4632 }
4620 },4633 },
4634 "node_modules/raw-body/node_modules/iconv-lite": {
4635 "version": "0.4.24",
4636 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
4637 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
4638 "license": "MIT",
4639 "dependencies": {
4640 "safer-buffer": ">= 2.1.2 < 3"
4641 },
4642 "engines": {
4643 "node": ">=0.10.0"
4644 }
4645 },
4621 "node_modules/readable-stream": {4646 "node_modules/readable-stream": {
4622 "version": "2.3.8",4647 "version": "2.3.8",
4623 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",4648 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
package.json+2 -1
@@ -17,6 +17,7 @@
17 "google-translate-api-browser": "^3.0.1",17 "google-translate-api-browser": "^3.0.1",
18 "he": "^1.2.0",18 "he": "^1.2.0",
19 "helmet": "^7.1.0",19 "helmet": "^7.1.0",
20 "iconv-lite": "^0.6.3",
20 "ip-matching": "^2.1.2",21 "ip-matching": "^2.1.2",
21 "ipaddr.js": "^2.0.1",22 "ipaddr.js": "^2.0.1",
22 "jimp": "^0.22.10",23 "jimp": "^0.22.10",
@@ -32,7 +33,7 @@
32 "rate-limiter-flexible": "^5.0.0",33 "rate-limiter-flexible": "^5.0.0",
33 "response-time": "^2.3.2",34 "response-time": "^2.3.2",
34 "sanitize-filename": "^1.6.3",35 "sanitize-filename": "^1.6.3",
35 "sillytavern-transformers": "^2.14.6",36 "sillytavern-transformers": "2.14.6",
36 "simple-git": "^3.19.1",37 "simple-git": "^3.19.1",
37 "tiktoken": "^1.0.15",38 "tiktoken": "^1.0.15",
38 "vectra": "^0.2.2",39 "vectra": "^0.2.2",
public/css/character-group-overlay.css+1 -1
@@ -99,6 +99,6 @@
99}99}
100100
101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
102 width: 100px;102 width: unset;
103 padding: 0.25em;103 padding: 0.25em;
104}104}
public/index.html+5 -1
@@ -4121,6 +4121,10 @@
4121 <input id="world_import_dialog" type="checkbox" />4121 <input id="world_import_dialog" type="checkbox" />
4122 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>4122 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>
4123 </label>4123 </label>
4124 <label data-newbie-hidden class="checkbox_label" for="enable_auto_select_input" title="Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields." data-i18n="[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.">
4125 <input id="enable_auto_select_input" type="checkbox" />
4126 <small data-i18n="Auto-select Input Text">Auto-select Input Text</small>
4127 </label>
4124 <label class="checkbox_label" for="restore_user_input" title="Restore unsaved user input on page refresh." data-i18n="[title]Restore unsaved user input on page refresh">4128 <label class="checkbox_label" for="restore_user_input" title="Restore unsaved user input on page refresh." data-i18n="[title]Restore unsaved user input on page refresh">
4125 <input id="restore_user_input" type="checkbox" />4129 <input id="restore_user_input" type="checkbox" />
4126 <small data-i18n="Restore User Input">Restore User Input</small>4130 <small data-i18n="Restore User Input">Restore User Input</small>
@@ -5008,7 +5012,7 @@
5008 <div class="popup-crop-wrap">5012 <div class="popup-crop-wrap">
5009 <img class="popup-crop-image" src="">5013 <img class="popup-crop-image" src="">
5010 </div>5014 </div>
5011 <textarea class="popup-input text_pole result-control" rows="1" data-result="1" data-result-event="submit"></textarea>5015 <textarea class="popup-input text_pole result-control auto-select" rows="1" data-result="1" data-result-event="submit"></textarea>
5012 <div class="popup-inputs"></div>5016 <div class="popup-inputs"></div>
5013 <div class="popup-controls">5017 <div class="popup-controls">
5014 <div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>5018 <div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>
public/script.js+19 -8
@@ -5426,9 +5426,11 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5426 getMessage = fixMarkdown(getMessage, false);5426 getMessage = fixMarkdown(getMessage, false);
5427 }5427 }
54285428
5429 const nameToTrim2 = isImpersonate ? name1 : name2;5429 const nameToTrim2 = isImpersonate
5430 ? (!power_user.allow_name1_display ? name1 : '')
5431 : (!power_user.allow_name2_display ? name2 : '');
54305432
5431 if (getMessage.startsWith(nameToTrim2 + ':')) {5433 if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) {
5432 getMessage = getMessage.replace(nameToTrim2 + ':', '');5434 getMessage = getMessage.replace(nameToTrim2 + ':', '');
5433 getMessage = getMessage.trimStart();5435 getMessage = getMessage.trimStart();
5434 }5436 }
@@ -8390,6 +8392,9 @@ const CONNECT_API_MAP = {
8390 },8392 },
8391};8393};
83928394
8395// Collect all unique API names in an array
8396export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8397
8393// Fill connections map from textgen_types and chat_completion_sources8398// Fill connections map from textgen_types and chat_completion_sources
8394for (const textGenType of Object.values(textgen_types)) {8399for (const textGenType of Object.values(textgen_types)) {
8395 if (CONNECT_API_MAP[textGenType]) continue;8400 if (CONNECT_API_MAP[textGenType]) continue;
@@ -8964,9 +8969,6 @@ jQuery(async function () {
8964 return '';8969 return '';
8965 }8970 }
89668971
8967 // Collect all unique API names in an array
8968 const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8969
8970 SlashCommandParser.addCommandObject(SlashCommand.fromProps({8972 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
8971 name: 'dupe',8973 name: 'dupe',
8972 callback: duplicateCharacter,8974 callback: duplicateCharacter,
@@ -8975,13 +8977,13 @@ jQuery(async function () {
8975 SlashCommandParser.addCommandObject(SlashCommand.fromProps({8977 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
8976 name: 'api',8978 name: 'api',
8977 callback: connectAPISlash,8979 callback: connectAPISlash,
8980 returns: 'the current API',
8978 unnamedArgumentList: [8981 unnamedArgumentList: [
8979 SlashCommandArgument.fromProps({8982 SlashCommandArgument.fromProps({
8980 description: 'API to connect to',8983 description: 'API to connect to',
8981 typeList: [ARGUMENT_TYPE.STRING],8984 typeList: [ARGUMENT_TYPE.STRING],
8982 isRequired: false,
8983 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>8985 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
8984 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIs.findIndex(x => x === selected)),8986 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
8985 selected[0].toUpperCase() ?? enumIcons.default)),8987 selected[0].toUpperCase() ?? enumIcons.default)),
8986 }),8988 }),
8987 ],8989 ],
@@ -10655,6 +10657,15 @@ jQuery(async function () {
10655 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);10657 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
10656 /* $('#set_character_world').on('click', openCharacterWorldPopup); */10658 /* $('#set_character_world').on('click', openCharacterWorldPopup); */
1065710659
10660 $(document).on('focus', 'input.auto-select, textarea.auto-select', function () {
10661 if (!power_user.enable_auto_select_input) return;
10662 const control = $(this)[0];
10663 if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {
10664 control.select();
10665 console.debug('Auto-selecting content of input control', control);
10666 }
10667 });
10668
10658 $(document).keyup(function (e) {10669 $(document).keyup(function (e) {
10659 if (e.key === 'Escape') {10670 if (e.key === 'Escape') {
10660 const isEditVisible = $('#curEditTextarea').is(':visible');10671 const isEditVisible = $('#curEditTextarea').is(':visible');
@@ -10738,7 +10749,7 @@ jQuery(async function () {
10738 }10749 }
10739 } break;10750 } break;
10740 case 'import_tags': {10751 case 'import_tags': {
10741 await importTags(characters[this_chid], { forceShow: true });10752 await importTags(characters[this_chid], { importSetting: tag_import_setting.ASK });
10742 } break;10753 } break;
10743 /*case 'delete_button':10754 /*case 'delete_button':
10744 popup_type = "del_ch";10755 popup_type = "del_ch";
public/scripts/BulkEditOverlay.js+34 -4
@@ -18,7 +18,7 @@ import {
18import { favsToHotswap } from './RossAscends-mods.js';18import { favsToHotswap } from './RossAscends-mods.js';
19import { hideLoader, showLoader } from './loader.js';19import { hideLoader, showLoader } from './loader.js';
20import { convertCharacterToPersona } from './personas.js';20import { convertCharacterToPersona } from './personas.js';
21import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap } from './tags.js';21import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
2222
23/**23/**
24 * Static object representing the actions of the24 * Static object representing the actions of the
@@ -197,10 +197,10 @@ class BulkTagPopupHandler {
197 #getHtml = () => {197 #getHtml = () => {
198 const characterData = JSON.stringify({ characterIds: this.characterIds });198 const characterData = JSON.stringify({ characterIds: this.characterIds });
199 return `<div id="bulk_tag_shadow_popup">199 return `<div id="bulk_tag_shadow_popup">
200 <div id="bulk_tag_popup">200 <div id="bulk_tag_popup" class="wider_dialogue_popup">
201 <div id="bulk_tag_popup_holder">201 <div id="bulk_tag_popup_holder">
202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters.</small>203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters. Import all or existing tags for all selected characters.</small>
204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
205 <br>205 <br>
206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
@@ -219,6 +219,12 @@ class BulkTagPopupHandler {
219 <i class="fa-solid fa-trash-can margin-right-10px"></i>219 <i class="fa-solid fa-trash-can margin-right-10px"></i>
220 Mutual220 Mutual
221 </div>221 </div>
222 <div id="bulk_tag_popup_import_all_tags" class="menu_button" title="Import all tags from selected characters" data-i18n="[title]Import all tags from selected characters">
223 Import All
224 </div>
225 <div id="bulk_tag_popup_import_existing_tags" class="menu_button" title="Import existing tags from selected characters" data-i18n="[title]Import existing tags from selected characters">
226 Import Existing
227 </div>
222 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>228 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
223 </div>229 </div>
224 </div>230 </div>
@@ -254,6 +260,30 @@ class BulkTagPopupHandler {
254 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));260 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
255 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));261 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
256 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));262 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
263 document.querySelector('#bulk_tag_popup_import_all_tags').addEventListener('click', this.importAllTags.bind(this));
264 document.querySelector('#bulk_tag_popup_import_existing_tags').addEventListener('click', this.importExistingTags.bind(this));
265 }
266
267 /**
268 * Import existing tags for all selected characters
269 */
270 async importExistingTags() {
271 for (const characterId of this.characterIds) {
272 await importTags(characters[characterId], { importSetting: tag_import_setting.ONLY_EXISTING });
273 }
274
275 $('#bulkTagList').empty();
276 }
277
278 /**
279 * Import all tags for all selected characters
280 */
281 async importAllTags() {
282 for (const characterId of this.characterIds) {
283 await importTags(characters[characterId], { importSetting: tag_import_setting.ALL });
284 }
285
286 $('#bulkTagList').empty();
257 }287 }
258288
259 /**289 /**
@@ -570,7 +600,7 @@ class BulkEditOverlay {
570 this.container.removeEventListener('mouseup', cancelHold);600 this.container.removeEventListener('mouseup', cancelHold);
571 this.container.removeEventListener('touchend', cancelHold);601 this.container.removeEventListener('touchend', cancelHold);
572 },602 },
573 BulkEditOverlay.longPressDelay);603 BulkEditOverlay.longPressDelay);
574 };604 };
575605
576 handleLongPressEnd = (event) => {606 handleLongPressEnd = (event) => {
public/scripts/RossAscends-mods.js+5 -4
@@ -954,6 +954,11 @@ export function initRossMods() {
954 * @param {KeyboardEvent} event954 * @param {KeyboardEvent} event
955 */955 */
956 async function processHotkeys(event) {956 async function processHotkeys(event) {
957 // Default hotkeys and shortcuts shouldn't work if any popup is currently open
958 if (Popup.util.isPopupOpen()) {
959 return;
960 }
961
957 //Enter to send when send_textarea in focus962 //Enter to send when send_textarea in focus
958 if (document.activeElement == hotkeyTargets['send_textarea']) {963 if (document.activeElement == hotkeyTargets['send_textarea']) {
959 const sendOnEnter = shouldSendOnEnter();964 const sendOnEnter = shouldSendOnEnter();
@@ -1107,10 +1112,6 @@ export function initRossMods() {
1107 }1112 }
11081113
1109 if (event.key == 'Escape') { //closes various panels1114 if (event.key == 'Escape') { //closes various panels
1110 // Do not close panels if we are currently inside a popup
1111 if (Popup.util.isPopupOpen())
1112 return;
1113
1114 //dont override Escape hotkey functions from script.js1115 //dont override Escape hotkey functions from script.js
1115 //"close edit box" and "cancel stream generation".1116 //"close edit box" and "cancel stream generation".
1116 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {1117 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {
public/scripts/extensions/caption/settings.html+1 -0
@@ -38,6 +38,7 @@
38 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>38 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
39 <option data-type="openai" value="gpt-4o">gpt-4o</option>39 <option data-type="openai" value="gpt-4o">gpt-4o</option>
40 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>40 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>
41 <option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
41 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>42 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
42 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>43 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
43 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>44 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
public/scripts/extensions/vectors/index.js+7 -6
@@ -401,7 +401,7 @@ async function processFiles(chat) {
401 const dataBankCollectionIds = await ingestDataBankAttachments();401 const dataBankCollectionIds = await ingestDataBankAttachments();
402402
403 if (dataBankCollectionIds.length) {403 if (dataBankCollectionIds.length) {
404 const queryText = await getQueryText(chat);404 const queryText = await getQueryText(chat, 'file');
405 await injectDataBankChunks(queryText, dataBankCollectionIds);405 await injectDataBankChunks(queryText, dataBankCollectionIds);
406 }406 }
407407
@@ -435,7 +435,7 @@ async function processFiles(chat) {
435 await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size, settings.overlap_percent);435 await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size, settings.overlap_percent);
436 }436 }
437437
438 const queryText = await getQueryText(chat);438 const queryText = await getQueryText(chat, 'file');
439 const fileChunks = await retrieveFileChunks(queryText, collectionId);439 const fileChunks = await retrieveFileChunks(queryText, collectionId);
440440
441 message.mes = `${fileChunks}\n\n${message.mes}`;441 message.mes = `${fileChunks}\n\n${message.mes}`;
@@ -596,7 +596,7 @@ async function rearrangeChat(chat) {
596 return;596 return;
597 }597 }
598598
599 const queryText = await getQueryText(chat);599 const queryText = await getQueryText(chat, 'chat');
600600
601 if (queryText.length === 0) {601 if (queryText.length === 0) {
602 console.debug('Vectors: No text to query');602 console.debug('Vectors: No text to query');
@@ -683,15 +683,16 @@ const onChatEvent = debounce(async () => await moduleWorker.update(), debounce_t
683/**683/**
684 * Gets the text to query from the chat684 * Gets the text to query from the chat
685 * @param {object[]} chat Chat messages685 * @param {object[]} chat Chat messages
686 * @param {'file'|'chat'|'world-info'} initiator Initiator of the query
686 * @returns {Promise<string>} Text to query687 * @returns {Promise<string>} Text to query
687 */688 */
688async function getQueryText(chat) {689async function getQueryText(chat, initiator) {
689 let queryText = '';690 let queryText = '';
690 let i = 0;691 let i = 0;
691692
692 let hashedMessages = chat.map(x => ({ text: String(substituteParams(x.mes)) }));693 let hashedMessages = chat.map(x => ({ text: String(substituteParams(x.mes)) }));
693694
694 if (settings.summarize && settings.summarize_sent) {695 if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) {
695 hashedMessages = await summarize(hashedMessages, settings.summary_source);696 hashedMessages = await summarize(hashedMessages, settings.summary_source);
696 }697 }
697698
@@ -1279,7 +1280,7 @@ async function activateWorldInfo(chat) {
1279 }1280 }
12801281
1281 // Perform a multi-query1282 // Perform a multi-query
1282 const queryText = await getQueryText(chat);1283 const queryText = await getQueryText(chat, 'world-info');
12831284
1284 if (queryText.length === 0) {1285 if (queryText.length === 0) {
1285 console.debug('Vectors: No text to query for WI');1286 console.debug('Vectors: No text to query for WI');
public/scripts/openai.js+2 -1
@@ -4763,12 +4763,13 @@ export function isImageInliningSupported() {
4763 'gpt-4-turbo',4763 'gpt-4-turbo',
4764 'gpt-4o',4764 'gpt-4o',
4765 'gpt-4o-mini',4765 'gpt-4o-mini',
4766 'chatgpt-4o-latest',
4766 'yi-vision',4767 'yi-vision',
4767 ];4768 ];
47684769
4769 switch (oai_settings.chat_completion_source) {4770 switch (oai_settings.chat_completion_source) {
4770 case chat_completion_sources.OPENAI:4771 case chat_completion_sources.OPENAI:
4771 return visionSupportedModels.some(model => oai_settings.openai_model.includes(model) && !oai_settings.openai_model.includes('chatgpt-4o-latest') && !oai_settings.openai_model.includes('gpt-4-turbo-preview'));4772 return visionSupportedModels.some(model => oai_settings.openai_model.includes(model) && !oai_settings.openai_model.includes('gpt-4-turbo-preview'));
4772 case chat_completion_sources.MAKERSUITE:4773 case chat_completion_sources.MAKERSUITE:
4773 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));4774 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
4774 case chat_completion_sources.CLAUDE:4775 case chat_completion_sources.CLAUDE:
public/scripts/popup.js+1 -1
@@ -596,7 +596,7 @@ export class Popup {
596596
597 /** @returns {boolean} Checks if any modal popup dialog is open */597 /** @returns {boolean} Checks if any modal popup dialog is open */
598 isPopupOpen() {598 isPopupOpen() {
599 return Popup.util.popups.length > 0;599 return Popup.util.popups.filter(x => x.dlg.hasAttribute('open')).length > 0;
600 },600 },
601601
602 /**602 /**
public/scripts/power-user.js+8 -0
@@ -202,6 +202,7 @@ let power_user = {
202 trim_spaces: true,202 trim_spaces: true,
203 relaxed_api_urls: false,203 relaxed_api_urls: false,
204 world_import_dialog: true,204 world_import_dialog: true,
205 enable_auto_select_input: false,
205 tag_import_setting: tag_import_setting.ASK,206 tag_import_setting: tag_import_setting.ASK,
206 disable_group_trimming: false,207 disable_group_trimming: false,
207 single_line: false,208 single_line: false,
@@ -1611,6 +1612,7 @@ async function loadPowerUserSettings(settings, data) {
1611 $('#single_line').prop('checked', power_user.single_line);1612 $('#single_line').prop('checked', power_user.single_line);
1612 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);1613 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
1613 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);1614 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
1615 $('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);
1614 $('#trim_spaces').prop('checked', power_user.trim_spaces);1616 $('#trim_spaces').prop('checked', power_user.trim_spaces);
1615 $('#continue_on_send').prop('checked', power_user.continue_on_send);1617 $('#continue_on_send').prop('checked', power_user.continue_on_send);
1616 $('#quick_continue').prop('checked', power_user.quick_continue);1618 $('#quick_continue').prop('checked', power_user.quick_continue);
@@ -3788,6 +3790,12 @@ $(document).ready(() => {
3788 saveSettingsDebounced();3790 saveSettingsDebounced();
3789 });3791 });
37903792
3793 $('#enable_auto_select_input').on('input', function () {
3794 const value = !!$(this).prop('checked');
3795 power_user.enable_auto_select_input = value;
3796 saveSettingsDebounced();
3797 });
3798
3791 $('#spoiler_free_mode').on('input', function () {3799 $('#spoiler_free_mode').on('input', function () {
3792 power_user.spoiler_free_mode = !!$(this).prop('checked');3800 power_user.spoiler_free_mode = !!$(this).prop('checked');
3793 switchSpoilerMode();3801 switchSpoilerMode();
public/scripts/showdown-underscore.js+13 -3
@@ -7,9 +7,19 @@ export const markdownUnderscoreExt = () => {
7 }7 }
88
9 return [{9 return [{
10 type: 'lang',10 type: 'output',
11 regex: new RegExp('\\b(?<!_)_(?!_)(.*?)(?<!_)_(?!_)\\b', 'g'),11 regex: new RegExp('(<code>[\\s\\S]*?<\\/code>)|(?<!\\S)_(?!_)([^_\\n]+?)(?<!_)_(?!\\w)', 'g'),
12 replace: '<em>$1</em>',12 replace: function(match, codeContent, italicContent) {
13 if (codeContent) {
14 // If it's inside <code> tags, return unchanged
15 return match;
16 } else if (italicContent) {
17 // If it's an italic group, apply the replacement
18 return '<em>' + italicContent + '</em>';
19 }
20 // If none of the conditions are met, return the original match
21 return match;
22 },
13 }];23 }];
14 } catch (e) {24 } catch (e) {
15 console.error('Error in Showdown-underscore extension:', e);25 console.error('Error in Showdown-underscore extension:', e);
public/scripts/slash-commands.js+212 -25
@@ -1,7 +1,9 @@
1import {1import {
2 Generate,2 Generate,
3 UNIQUE_APIS,
3 activateSendButtons,4 activateSendButtons,
4 addOneMessage,5 addOneMessage,
6 api_server,
5 callPopup,7 callPopup,
6 characters,8 characters,
7 chat,9 chat,
@@ -49,8 +51,8 @@ import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSel
49import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';51import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
50import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';52import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
51import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';53import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
52import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';54import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
53import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';55import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
54import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';56import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
55import { registerVariableCommands, resolveVariable } from './variables.js';57import { registerVariableCommands, resolveVariable } from './variables.js';
56import { background_settings } from './backgrounds.js';58import { background_settings } from './backgrounds.js';
@@ -1496,8 +1498,9 @@ export function initDefaultSlashCommands() {
1496 ],1498 ],
1497 helpString: 'Sets the specified prompt manager entry/entries on or off.',1499 helpString: 'Sets the specified prompt manager entry/entries on or off.',
1498 }));1500 }));
1499 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',1501 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1500 callback: async()=>((await showFontAwesomePicker()) ?? false).toString(),1502 name: 'pick-icon',
1503 callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
1501 returns: 'The chosen icon name or false if cancelled.',1504 returns: 'The chosen icon name or false if cancelled.',
1502 helpString: `1505 helpString: `
1503 <div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>1506 <div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>
@@ -1511,6 +1514,72 @@ export function initDefaultSlashCommands() {
1511 </div>1514 </div>
1512 `,1515 `,
1513 }));1516 }));
1517 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1518 name: 'api-url',
1519 callback: setApiUrlCallback,
1520 returns: 'the current API url',
1521 aliases: ['server'],
1522 namedArgumentList: [
1523 SlashCommandNamedArgument.fromProps({
1524 name: 'api',
1525 description: 'API to set/get the URL for - if not provided, current API is used',
1526 typeList: [ARGUMENT_TYPE.STRING],
1527 enumList: [
1528 new SlashCommandEnumValue('custom', 'custom OpenAI-compatible', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'openai')), 'O'),
1529 new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'),
1530 ...Object.values(textgen_types).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')),
1531 ],
1532 }),
1533 SlashCommandNamedArgument.fromProps({
1534 name: 'connect',
1535 description: 'Whether to auto-connect to the API after setting the URL',
1536 typeList: [ARGUMENT_TYPE.BOOLEAN],
1537 defaultValue: 'true',
1538 enumList: commonEnumProviders.boolean('trueFalse')(),
1539 }),
1540 ],
1541 unnamedArgumentList: [
1542 SlashCommandArgument.fromProps({
1543 description: 'API url to connect to',
1544 typeList: [ARGUMENT_TYPE.STRING],
1545 }),
1546 ],
1547 helpString: `
1548 <div>
1549 Set the API url / server url for the currently selected API, including the port. If no argument is provided, it will return the current API url.
1550 </div>
1551 <div>
1552 If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API,
1553 or consider switching to it with <code>/api</code> first.
1554 </div>
1555 <div>
1556 This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible for the Chat Completion sources. If unsure which APIs are supported,
1557 check the auto-completion of the optional <code>api</code> argument of this command.
1558 </div>
1559 `,
1560 }));
1561 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1562 name: 'tokenizer',
1563 callback: selectTokenizerCallback,
1564 returns: 'current tokenizer',
1565 unnamedArgumentList: [
1566 SlashCommandArgument.fromProps({
1567 description: 'tokenizer name',
1568 typeList: [ARGUMENT_TYPE.STRING],
1569 enumList: getAvailableTokenizers().map(tokenizer =>
1570 new SlashCommandEnumValue(tokenizer.tokenizerKey, tokenizer.tokenizerName, enumTypes.enum, enumIcons.default)),
1571 }),
1572 ],
1573 helpString: `
1574 <div>
1575 Selects tokenizer by name. Gets the current tokenizer if no name is provided.
1576 </div>
1577 <div>
1578 <strong>Available tokenizers:</strong>
1579 <pre><code>${getAvailableTokenizers().map(t => t.tokenizerKey).join(', ')}</code></pre>
1580 </div>
1581 `,
1582 }));
15141583
1515 registerVariableCommands();1584 registerVariableCommands();
1516}1585}
@@ -1788,7 +1857,7 @@ async function popupCallback(args, value) {
1788 return String(value);1857 return String(value);
1789}1858}
17901859
1791function getMessagesCallback(args, value) {1860async function getMessagesCallback(args, value) {
1792 const includeNames = !isFalseBoolean(args?.names);1861 const includeNames = !isFalseBoolean(args?.names);
1793 const includeHidden = isTrueBoolean(args?.hidden);1862 const includeHidden = isTrueBoolean(args?.hidden);
1794 const role = args?.role;1863 const role = args?.role;
@@ -1821,33 +1890,34 @@ function getMessagesCallback(args, value) {
1821 throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);1890 throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
1822 };1891 };
18231892
1824 const messages = [];1893 const processMessage = async (mesId) => {
18251894 const msg = chat[mesId];
1826 for (let messageId = range.start; messageId <= range.end; messageId++) {1895 if (!msg) {
1827 const message = chat[messageId];1896 console.warn(`WARN: No message found with ID ${mesId}`);
1828 if (!message) {1897 return null;
1829 console.warn(`WARN: No message found with ID ${messageId}`);
1830 continue;
1831 }1898 }
18321899
1833 if (role && !filterByRole(message)) {1900 if (role && !filterByRole(msg)) {
1834 console.debug(`/messages: Skipping message with ID ${messageId} due to role filter`);1901 console.debug(`/messages: Skipping message with ID ${mesId} due to role filter`);
1835 continue;1902 return null;
1836 }1903 }
18371904
1838 if (!includeHidden && message.is_system) {1905 if (!includeHidden && msg.is_system) {
1839 console.debug(`/messages: Skipping hidden message with ID ${messageId}`);1906 console.debug(`/messages: Skipping hidden message with ID ${mesId}`);
1840 continue;1907 return null;
1841 }1908 }
18421909
1843 if (includeNames) {1910 return includeNames ? `${msg.name}: ${msg.mes}` : msg.mes;
1844 messages.push(`${message.name}: ${message.mes}`);1911 };
1845 } else {1912
1846 messages.push(message.mes);1913 const messagePromises = [];
1847 }1914
1848 }1915 for (let rInd = range.start; rInd <= range.end; ++rInd)
1916 messagePromises.push(processMessage(rInd));
18491917
1850 return messages.join('\n\n');1918 const messages = await Promise.all(messagePromises);
1919
1920 return messages.filter(m => m !== null).join('\n\n');
1851}1921}
18521922
1853async function runCallback(args, name) {1923async function runCallback(args, name) {
@@ -3418,6 +3488,123 @@ function setPromptEntryCallback(args, targetState) {
3418 return '';3488 return '';
3419}3489}
34203490
3491/**
3492 * Sets the API URL and triggers the text generation web UI button click.
3493 *
3494 * @param {object} args - named args
3495 * @param {string?} [args.api=null] - the API name to set/get the URL for
3496 * @param {string?} [args.connect=true] - whether to connect to the API after setting
3497 * @param {string} url - the API URL to set
3498 * @returns {Promise<string>}
3499 */
3500async function setApiUrlCallback({ api = null, connect = 'true' }, url) {
3501 const autoConnect = isTrueBoolean(connect);
3502
3503 // Special handling for Chat Completion Custom OpenAI compatible, that one can also support API url handling
3504 const isCurrentlyCustomOpenai = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
3505 if (api === chat_completion_sources.CUSTOM || (!api && isCurrentlyCustomOpenai)) {
3506 if (!url) {
3507 return oai_settings.custom_url ?? '';
3508 }
3509
3510 if (!isCurrentlyCustomOpenai && autoConnect) {
3511 toastr.warning('Custom OpenAI API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
3512 return '';
3513 }
3514
3515 $('#custom_api_url_text').val(url).trigger('input');
3516
3517 if (autoConnect) {
3518 $('#api_button_openai').trigger('click');
3519 }
3520
3521 return url;
3522 }
3523
3524 // Special handling for Kobold Classic API
3525 const isCurrentlyKoboldClassic = main_api === 'kobold';
3526 if (api === 'kobold' || (!api && isCurrentlyKoboldClassic)) {
3527 if (!url) {
3528 return api_server ?? '';
3529 }
3530
3531 if (!isCurrentlyKoboldClassic && autoConnect) {
3532 toastr.warning('Kobold Classic API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
3533 return '';
3534 }
3535
3536 $('#api_url_text').val(url).trigger('input');
3537 // trigger blur debounced, so we hide the autocomplete menu
3538 setTimeout(() => $('#api_url_text').trigger('blur'), 1);
3539
3540 if (autoConnect) {
3541 $('#api_button').trigger('click');
3542 }
3543
3544 return api_server ?? '';
3545 }
3546
3547 // Do some checks and get the api type we are targeting with this command
3548 if (api && !Object.values(textgen_types).includes(api)) {
3549 toastr.warning(`API '${api}' is not a valid text_gen API.`);
3550 return '';
3551 }
3552 if (!api && !Object.values(textgen_types).includes(textgenerationwebui_settings.type)) {
3553 toastr.warning(`API '${textgenerationwebui_settings.type}' is not a valid text_gen API.`);
3554 return '';
3555 }
3556 if (api && url && autoConnect && api !== textgenerationwebui_settings.type) {
3557 toastr.warning(`API '${api}' is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
3558 return '';
3559 }
3560 const type = api || textgenerationwebui_settings.type;
3561
3562 const inputSelector = SERVER_INPUTS[type];
3563 if (!inputSelector) {
3564 toastr.warning(`API '${type}' does not have a server url input.`);
3565 return '';
3566 }
3567
3568 // If no url was provided, return the current one
3569 if (!url) {
3570 return textgenerationwebui_settings.server_urls[type] ?? '';
3571 }
3572
3573 // else, we want to actually set the url
3574 $(inputSelector).val(url).trigger('input');
3575 // trigger blur debounced, so we hide the autocomplete menu
3576 setTimeout(() => $(inputSelector).trigger('blur'), 1);
3577
3578 // Trigger the auto connect via connect button, if requested
3579 if (autoConnect) {
3580 $('#api_button_textgenerationwebui').trigger('click');
3581 }
3582
3583 // We still re-acquire the value, as it might have been modified by the validation on connect
3584 return textgenerationwebui_settings.server_urls[type] ?? '';
3585}
3586
3587async function selectTokenizerCallback(_, name) {
3588 if (!name) {
3589 return getAvailableTokenizers().find(tokenizer => tokenizer.tokenizerId === power_user.tokenizer)?.tokenizerKey ?? '';
3590 }
3591
3592 const tokenizers = getAvailableTokenizers();
3593 const fuse = new Fuse(tokenizers, { keys: ['tokenizerKey', 'tokenizerName'] });
3594 const result = fuse.search(name);
3595
3596 if (result.length === 0) {
3597 toastr.warning(`Tokenizer "${name}" not found`);
3598 return '';
3599 }
3600
3601 /** @type {import('./tokenizers.js').Tokenizer} */
3602 const foundTokenizer = result[0].item;
3603 selectTokenizer(foundTokenizer.tokenizerId);
3604
3605 return foundTokenizer.tokenizerKey;
3606}
3607
3421export let isExecutingCommandsFromChatInput = false;3608export let isExecutingCommandsFromChatInput = false;
3422export let commandsFromChatInputAbortController;3609export let commandsFromChatInputAbortController;
34233610
public/scripts/tags.js+8 -8
@@ -708,12 +708,12 @@ const ANTI_TROLL_MAX_TAGS = 15;
708 *708 *
709 * @param {Character} character - The character709 * @param {Character} character - The character
710 * @param {object} [options] - Options710 * @param {object} [options] - Options
711 * @param {boolean} [options.forceShow=false] - Whether to force showing the import dialog711 * @param {tag_import_setting} [options.importSetting=null] - Force a tag import setting
712 * @returns {Promise<boolean>} Boolean indicating whether any tag was imported712 * @returns {Promise<boolean>} Boolean indicating whether any tag was imported
713 */713 */
714async function importTags(character, { forceShow = false } = {}) {714async function importTags(character, { importSetting = null } = {}) {
715 // Gather the tags to import based on the selected setting715 // Gather the tags to import based on the selected setting
716 const tagNamesToImport = await handleTagImport(character, { forceShow });716 const tagNamesToImport = await handleTagImport(character, { importSetting });
717 if (!tagNamesToImport?.length) {717 if (!tagNamesToImport?.length) {
718 console.debug('No tags to import');718 console.debug('No tags to import');
719 return;719 return;
@@ -732,10 +732,10 @@ async function importTags(character, { forceShow = false } = {}) {
732 *732 *
733 * @param {Character} character - The character733 * @param {Character} character - The character
734 * @param {object} [options] - Options734 * @param {object} [options] - Options
735 * @param {boolean} [options.forceShow=false] - Whether to force showing the import dialog735 * @param {tag_import_setting} [options.importSetting=null] - Force a tag import setting
736 * @returns {Promise<string[]>} Array of strings representing the tags to import736 * @returns {Promise<string[]>} Array of strings representing the tags to import
737 */737 */
738async function handleTagImport(character, { forceShow = false } = {}) {738async function handleTagImport(character, { importSetting = null } = {}) {
739 /** @type {string[]} */739 /** @type {string[]} */
740 const importTags = character.tags.map(t => t.trim()).filter(t => t)740 const importTags = character.tags.map(t => t.trim()).filter(t => t)
741 .filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))741 .filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))
@@ -745,9 +745,9 @@ async function handleTagImport(character, { forceShow = false } = {}) {
745 .map(newTag);745 .map(newTag);
746 const folderTags = getOpenBogusFolders();746 const folderTags = getOpenBogusFolders();
747747
748 // Choose the setting for this dialog. If from settings, verify the setting really exists, otherwise take "ASK".748 // Choose the setting for this dialog. First check override, then saved setting or finally use "ASK".
749 const setting = forceShow ? tag_import_setting.ASK749 const setting = importSetting ? importSetting :
750 : Object.values(tag_import_setting).find(setting => setting === power_user.tag_import_setting) ?? tag_import_setting.ASK;750 Object.values(tag_import_setting).find(setting => setting === power_user.tag_import_setting) ?? tag_import_setting.ASK;
751751
752 switch (setting) {752 switch (setting) {
753 case tag_import_setting.ALL:753 case tag_import_setting.ALL:
public/scripts/textgen-settings.js+30 -2
@@ -95,7 +95,7 @@ let DREAMGEN_SERVER = 'https://dreamgen.com';
95let OPENROUTER_SERVER = 'https://openrouter.ai/api';95let OPENROUTER_SERVER = 'https://openrouter.ai/api';
96let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';96let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';
9797
98const SERVER_INPUTS = {98export const SERVER_INPUTS = {
99 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',99 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
100 [textgen_types.VLLM]: '#vllm_api_url_text',100 [textgen_types.VLLM]: '#vllm_api_url_text',
101 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',101 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',
@@ -1071,6 +1071,34 @@ function getLogprobsNumber() {
1071 return 10;1071 return 10;
1072}1072}
10731073
1074/**
1075 * Replaces {{macro}} in a comma-separated or serialized JSON array string.
1076 * @param {string} str Input string
1077 * @returns {string} Output string
1078 */
1079function replaceMacrosInList(str) {
1080 if (!str || typeof str !== 'string') {
1081 return str;
1082 }
1083
1084 try {
1085 const array = JSON.parse(str);
1086 if (!Array.isArray(array)) {
1087 throw new Error('Not an array');
1088 }
1089 for (let i = 0; i < array.length; i++) {
1090 array[i] = substituteParams(array[i]);
1091 }
1092 return JSON.stringify(array);
1093 } catch {
1094 const array = str.split(',');
1095 for (let i = 0; i < array.length; i++) {
1096 array[i] = substituteParams(array[i]);
1097 }
1098 return array.join(',');
1099 }
1100}
1101
1074export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {1102export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
1075 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';1103 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
1076 const dynatemp = isDynamicTemperatureSupported();1104 const dynatemp = isDynamicTemperatureSupported();
@@ -1110,7 +1138,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1110 'dry_allowed_length': settings.dry_allowed_length,1138 'dry_allowed_length': settings.dry_allowed_length,
1111 'dry_multiplier': settings.dry_multiplier,1139 'dry_multiplier': settings.dry_multiplier,
1112 'dry_base': settings.dry_base,1140 'dry_base': settings.dry_base,
1113 'dry_sequence_breakers': settings.dry_sequence_breakers,1141 'dry_sequence_breakers': replaceMacrosInList(settings.dry_sequence_breakers),
1114 'dry_penalty_last_n': settings.dry_penalty_last_n,1142 'dry_penalty_last_n': settings.dry_penalty_last_n,
1115 'max_tokens_second': settings.max_tokens_second,1143 'max_tokens_second': settings.max_tokens_second,
1116 'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined,1144 'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined,
public/scripts/tokenizers.js+40 -2
@@ -148,9 +148,45 @@ async function resetTokenCache() {
148}148}
149149
150/**150/**
151 * @typedef {object} Tokenizer
152 * @property {number} tokenizerId - The id of the tokenizer option
153 * @property {string} tokenizerKey - Internal name/key of the tokenizer
154 * @property {string} tokenizerName - Human-readable detailed name of the tokenizer (as displayed in the UI)
155 */
156
157/**
158 * Gets all tokenizers available to the user.
159 * @returns {Tokenizer[]} Tokenizer info.
160 */
161export function getAvailableTokenizers() {
162 const tokenizerOptions = $('#tokenizer').find('option').toArray();
163 return tokenizerOptions.map(tokenizerOption => ({
164 tokenizerId: Number(tokenizerOption.value),
165 tokenizerKey: Object.entries(tokenizers).find(([_, value]) => value === Number(tokenizerOption.value))[0].toLocaleLowerCase(),
166 tokenizerName: tokenizerOption.text,
167 }))
168}
169
170/**
171 * Selects tokenizer if not already selected.
172 * @param {number} tokenizerId Tokenizer ID.
173 */
174export function selectTokenizer(tokenizerId) {
175 if (tokenizerId !== power_user.tokenizer) {
176 const tokenizer = getAvailableTokenizers().find(tokenizer => tokenizer.tokenizerId === tokenizerId);
177 if (!tokenizer) {
178 console.warn('Failed to find tokenizer with id', tokenizerId);
179 return;
180 }
181 $('#tokenizer').val(tokenizer.tokenizerId).trigger('change');
182 toastr.info(`Tokenizer: "${tokenizer.tokenizerName}" selected`);
183 }
184}
185
186/**
151 * Gets the friendly name of the current tokenizer.187 * Gets the friendly name of the current tokenizer.
152 * @param {string} forApi API to get the tokenizer for. Defaults to the main API.188 * @param {string} forApi API to get the tokenizer for. Defaults to the main API.
153 * @returns { { tokenizerName: string, tokenizerId: number } } Tokenizer info189 * @returns {Tokenizer} Tokenizer info
154 */190 */
155export function getFriendlyTokenizerName(forApi) {191export function getFriendlyTokenizerName(forApi) {
156 if (!forApi) {192 if (!forApi) {
@@ -185,7 +221,9 @@ export function getFriendlyTokenizerName(forApi) {
185 ? tokenizers.OPENAI221 ? tokenizers.OPENAI
186 : tokenizerId;222 : tokenizerId;
187223
188 return { tokenizerName, tokenizerId };224 const tokenizerKey = Object.entries(tokenizers).find(([_, value]) => value === tokenizerId)[0].toLocaleLowerCase();
225
226 return { tokenizerName, tokenizerKey, tokenizerId };
189}227}
190228
191/**229/**
server.js+7 -9
@@ -787,17 +787,17 @@ function logSecurityAlert(message) {
787 */787 */
788function handleServerListenFail(v6Failed, v4Failed) {788function handleServerListenFail(v6Failed, v4Failed) {
789 if (v6Failed && !enableIPv4) {789 if (v6Failed && !enableIPv4) {
790 console.error('fatal error: Failed to start server on IPv6 and IPv4 disabled');790 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
791 process.exit(1);791 process.exit(1);
792 }792 }
793793
794 if (v4Failed && !enableIPv6) {794 if (v4Failed && !enableIPv6) {
795 console.error('fatal error: Failed to start server on IPv4 and IPv6 disabled');795 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
796 process.exit(1);796 process.exit(1);
797 }797 }
798798
799 if (v6Failed && v4Failed) {799 if (v6Failed && v4Failed) {
800 console.error('fatal error: Failed to start server on both IPv6 and IPv4');800 console.error(color.red('fatal error: Failed to start server on both IPv6 and IPv4'));
801 process.exit(1);801 process.exit(1);
802 }802 }
803}803}
@@ -846,9 +846,8 @@ async function startHTTPorHTTPS() {
846 try {846 try {
847 await createFunc(tavernUrlV6);847 await createFunc(tavernUrlV6);
848 } catch (error) {848 } catch (error) {
849 if (enableIPv4) {849 console.error('non-fatal error: failed to start server on IPv6');
850 console.error('non-fatal error: failed to start server on IPv6', error);850 console.error(error);
851 }
852851
853 v6Failed = true;852 v6Failed = true;
854 }853 }
@@ -858,9 +857,8 @@ async function startHTTPorHTTPS() {
858 try {857 try {
859 await createFunc(tavernUrl);858 await createFunc(tavernUrl);
860 } catch (error) {859 } catch (error) {
861 if (enableIPv6) {860 console.error('non-fatal error: failed to start server on IPv4');
862 console.error('non-fatal error: failed to start server on IPv4', error);861 console.error(error);
863 }
864862
865 v4Failed = true;863 v4Failed = true;
866 }864 }
src/endpoints/translate.js+6 -3
@@ -1,6 +1,7 @@
1const fetch = require('node-fetch').default;1const fetch = require('node-fetch').default;
2const https = require('https');2const https = require('https');
3const express = require('express');3const express = require('express');
4const iconv = require('iconv-lite');
4const { readSecret, SECRET_KEYS } = require('./secrets');5const { readSecret, SECRET_KEYS } = require('./secrets');
5const { getConfigValue, uuidv4 } = require('../util');6const { getConfigValue, uuidv4 } = require('../util');
6const { jsonParser } = require('../express-common');7const { jsonParser } = require('../express-common');
@@ -80,16 +81,18 @@ router.post('/google', jsonParser, async (request, response) => {
80 const url = generateRequestUrl(text, { to: lang });81 const url = generateRequestUrl(text, { to: lang });
8182
82 https.get(url, (resp) => {83 https.get(url, (resp) => {
83 let data = '';84 const data = [];
8485
85 resp.on('data', (chunk) => {86 resp.on('data', (chunk) => {
86 data += chunk;87 data.push(chunk);
87 });88 });
8889
89 resp.on('end', () => {90 resp.on('end', () => {
90 try {91 try {
91 const result = normaliseResponse(JSON.parse(data));92 const decodedData = iconv.decode(Buffer.concat(data), 'utf-8');
93 const result = normaliseResponse(JSON.parse(decodedData));
92 console.log('Translated text: ' + result.text);94 console.log('Translated text: ' + result.text);
95 response.setHeader('Content-Type', 'text/plain; charset=utf-8');
93 return response.send(result.text);96 return response.send(result.text);
94 } catch (error) {97 } catch (error) {
95 console.log('Translation error', error);98 console.log('Translation error', error);
src/transformers.mjs+36 -3
@@ -1,6 +1,7 @@
1import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';1import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';
2import { getConfigValue } from './util.js';2import { getConfigValue } from './util.js';
3import path from 'path';3import path from 'path';
4import fs from 'fs';
45
5configureTransformers();6configureTransformers();
67
@@ -48,7 +49,7 @@ const tasks = {
48 configField: 'extras.textToSpeechModel',49 configField: 'extras.textToSpeechModel',
49 quantized: false,50 quantized: false,
50 },51 },
51}52};
5253
53/**54/**
54 * Gets a RawImage object from a base64-encoded image.55 * Gets a RawImage object from a base64-encoded image.
@@ -85,6 +86,36 @@ function getModelForTask(task) {
85 }86 }
86}87}
8788
89async function migrateCacheToDataDir() {
90 const oldCacheDir = path.join(process.cwd(), 'cache');
91 const newCacheDir = path.join(global.DATA_ROOT, '_cache');
92
93 if (!fs.existsSync(newCacheDir)) {
94 fs.mkdirSync(newCacheDir, { recursive: true });
95 }
96
97 if (fs.existsSync(oldCacheDir) && fs.statSync(oldCacheDir).isDirectory()) {
98 const files = fs.readdirSync(oldCacheDir);
99
100 if (files.length === 0) {
101 return;
102 }
103
104 console.log('Migrating model cache files to data directory. Please wait...');
105
106 for (const file of files) {
107 try {
108 const oldPath = path.join(oldCacheDir, file);
109 const newPath = path.join(newCacheDir, file);
110 fs.cpSync(oldPath, newPath, { recursive: true, force: true });
111 fs.rmSync(oldPath, { recursive: true, force: true });
112 } catch (error) {
113 console.warn('Failed to migrate cache file. The model will be re-downloaded.', error);
114 }
115 }
116 }
117}
118
88/**119/**
89 * Gets the transformers.js pipeline for a given task.120 * Gets the transformers.js pipeline for a given task.
90 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for121 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for
@@ -92,6 +123,8 @@ function getModelForTask(task) {
92 * @returns {Promise<Pipeline>} Pipeline for the task123 * @returns {Promise<Pipeline>} Pipeline for the task
93 */124 */
94async function getPipeline(task, forceModel = '') {125async function getPipeline(task, forceModel = '') {
126 await migrateCacheToDataDir();
127
95 if (tasks[task].pipeline) {128 if (tasks[task].pipeline) {
96 if (forceModel === '' || tasks[task].currentModel === forceModel) {129 if (forceModel === '' || tasks[task].currentModel === forceModel) {
97 return tasks[task].pipeline;130 return tasks[task].pipeline;
@@ -100,11 +133,11 @@ async function getPipeline(task, forceModel = '') {
100 await tasks[task].pipeline.dispose();133 await tasks[task].pipeline.dispose();
101 }134 }
102135
103 const cache_dir = path.join(process.cwd(), 'cache');136 const cacheDir = path.join(global.DATA_ROOT, '_cache');
104 const model = forceModel || getModelForTask(task);137 const model = forceModel || getModelForTask(task);
105 const localOnly = getConfigValue('extras.disableAutoDownload', false);138 const localOnly = getConfigValue('extras.disableAutoDownload', false);
106 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);139 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
107 const instance = await pipeline(task, model, { cache_dir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });140 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
108 tasks[task].pipeline = instance;141 tasks[task].pipeline = instance;
109 tasks[task].currentModel = model;142 tasks[task].currentModel = model;
110 return instance;143 return instance;
src/users.js+5 -11
@@ -20,12 +20,6 @@ const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
20const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');20const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2121
22/**22/**
23 * The root directory for user data.
24 * @type {string}
25 */
26let DATA_ROOT = './data';
27
28/**
29 * Cache for user directories.23 * Cache for user directories.
30 * @type {Map<string, UserDirectoryList>}24 * @type {Map<string, UserDirectoryList>}
31 */25 */
@@ -138,7 +132,7 @@ async function migrateUserData() {
138132
139 console.log();133 console.log();
140 console.log(color.magenta('Preparing to migrate user data...'));134 console.log(color.magenta('Preparing to migrate user data...'));
141 console.log(`All public data will be moved to the ${DATA_ROOT} directory.`);135 console.log(`All public data will be moved to the ${global.DATA_ROOT} directory.`);
142 console.log('This process may take a while depending on the amount of data to move.');136 console.log('This process may take a while depending on the amount of data to move.');
143 console.log(`Backups will be placed in the ${PUBLIC_DIRECTORIES.backups} directory.`);137 console.log(`Backups will be placed in the ${PUBLIC_DIRECTORIES.backups} directory.`);
144 console.log(`The process will start in ${TIMEOUT} seconds. Press Ctrl+C to cancel.`);138 console.log(`The process will start in ${TIMEOUT} seconds. Press Ctrl+C to cancel.`);
@@ -352,11 +346,11 @@ function toAvatarKey(handle) {
352 * @returns {Promise<void>}346 * @returns {Promise<void>}
353 */347 */
354async function initUserStorage(dataRoot) {348async function initUserStorage(dataRoot) {
355 DATA_ROOT = dataRoot;349 global.DATA_ROOT = dataRoot;
356 console.log('Using data root:', color.green(DATA_ROOT));350 console.log('Using data root:', color.green(global.DATA_ROOT));
357 console.log();351 console.log();
358 await storage.init({352 await storage.init({
359 dir: path.join(DATA_ROOT, '_storage'),353 dir: path.join(global.DATA_ROOT, '_storage'),
360 ttl: false, // Never expire354 ttl: false, // Never expire
361 });355 });
362356
@@ -457,7 +451,7 @@ function getUserDirectories(handle) {
457451
458 const directories = structuredClone(USER_DIRECTORY_TEMPLATE);452 const directories = structuredClone(USER_DIRECTORY_TEMPLATE);
459 for (const key in directories) {453 for (const key in directories) {
460 directories[key] = path.join(DATA_ROOT, handle, USER_DIRECTORY_TEMPLATE[key]);454 directories[key] = path.join(global.DATA_ROOT, handle, USER_DIRECTORY_TEMPLATE[key]);
461 }455 }
462 DIRECTORIES_CACHE.set(handle, directories);456 DIRECTORIES_CACHE.set(handle, directories);
463 return directories;457 return directories;