Merge branch 'staging' into feat/xtc

d77363cd7cd5410adb6b94511f725f2ec298e8e7

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

22 files changed, +475 -96Showing whitespace changes
index.d.ts+5 -0
@@ -9,6 +9,11 @@ declare global {
99 };
1010 }
1111 }
12+
13+ /**
14+ * The root directory for user data.
15+ */
16+ var DATA_ROOT: string;
1217}
1318
1419declare module 'express-session' {
package-lock.json+30 -5
@@ -27,6 +27,7 @@
2727 "google-translate-api-browser": "^3.0.1",
2828 "he": "^1.2.0",
2929 "helmet": "^7.1.0",
30+ "iconv-lite": "^0.6.3",
3031 "ip-matching": "^2.1.2",
3132 "ipaddr.js": "^2.0.1",
3233 "jimp": "^0.22.10",
@@ -42,7 +43,7 @@
4243 "rate-limiter-flexible": "^5.0.0",
4344 "response-time": "^2.3.2",
4445 "sanitize-filename": "^1.6.3",
4546 "sillytavern-transformers": "^2.14.6",
4647 "simple-git": "^3.19.1",
4748 "tiktoken": "^1.0.15",
4849 "vectra": "^0.2.2",
@@ -1492,6 +1493,18 @@
14921493 "node": ">= 0.8"
14931494 }
14941495 },
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+ },
14951508 "node_modules/boolbase": {
14961509 "version": "1.0.0",
14971510 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -3282,12 +3295,12 @@
32823295 }
32833296 },
32843297 "node_modules/iconv-lite": {
32853298 "version": "0.46.243",
32863299 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.46.243.tgz",
32873300 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
32883301 "license": "MIT",
32893302 "dependencies": {
32903303 "safer-buffer": ">= 2.1.2 < 3.0.0"
32913304 },
32923305 "engines": {
32933306 "node": ">=0.10.0"
@@ -4618,6 +4631,18 @@
46184631 "node": ">= 0.8"
46194632 }
46204633 },
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+ },
46214646 "node_modules/readable-stream": {
46224647 "version": "2.3.8",
46234648 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
package.json+2 -1
@@ -17,6 +17,7 @@
1717 "google-translate-api-browser": "^3.0.1",
1818 "he": "^1.2.0",
1919 "helmet": "^7.1.0",
20+ "iconv-lite": "^0.6.3",
2021 "ip-matching": "^2.1.2",
2122 "ipaddr.js": "^2.0.1",
2223 "jimp": "^0.22.10",
@@ -32,7 +33,7 @@
3233 "rate-limiter-flexible": "^5.0.0",
3334 "response-time": "^2.3.2",
3435 "sanitize-filename": "^1.6.3",
3536 "sillytavern-transformers": "^2.14.6",
3637 "simple-git": "^3.19.1",
3738 "tiktoken": "^1.0.15",
3839 "vectra": "^0.2.2",
public/css/character-group-overlay.css+1 -1
@@ -99,6 +99,6 @@
9999}
100100
101101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
102102 width: 100pxunset;
103103 padding: 0.25em;
104104}
public/index.html+5 -1
@@ -4121,6 +4121,10 @@
41214121 <input id="world_import_dialog" type="checkbox" />
41224122 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>
41234123 </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>
41244128 <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">
41254129 <input id="restore_user_input" type="checkbox" />
41264130 <small data-i18n="Restore User Input">Restore User Input</small>
@@ -5008,7 +5012,7 @@
50085012 <div class="popup-crop-wrap">
50095013 <img class="popup-crop-image" src="">
50105014 </div>
50115015 <textarea class="popup-input text_pole result-control auto-select" rows="1" data-result="1" data-result-event="submit"></textarea>
50125016 <div class="popup-inputs"></div>
50135017 <div class="popup-controls">
50145018 <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
54265426 getMessage = fixMarkdown(getMessage, false);
54275427 }
54285428
54295429 const nameToTrim2 = isImpersonate ? name1 : name2;
5430+ ? (!power_user.allow_name1_display ? name1 : '')
5431+ : (!power_user.allow_name2_display ? name2 : '');
54305432
54315433 if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) {
54325434 getMessage = getMessage.replace(nameToTrim2 + ':', '');
54335435 getMessage = getMessage.trimStart();
54345436 }
@@ -8390,6 +8392,9 @@ const CONNECT_API_MAP = {
83908392 },
83918393};
83928394
8395+// Collect all unique API names in an array
8396+export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8397+
83938398// Fill connections map from textgen_types and chat_completion_sources
83948399for (const textGenType of Object.values(textgen_types)) {
83958400 if (CONNECT_API_MAP[textGenType]) continue;
@@ -8964,9 +8969,6 @@ jQuery(async function () {
89648969 return '';
89658970 }
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-
89708972 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89718973 name: 'dupe',
89728974 callback: duplicateCharacter,
@@ -8975,13 +8977,13 @@ jQuery(async function () {
89758977 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89768978 name: 'api',
89778979 callback: connectAPISlash,
8980+ returns: 'the current API',
89788981 unnamedArgumentList: [
89798982 SlashCommandArgument.fromProps({
89808983 description: 'API to connect to',
89818984 typeList: [ARGUMENT_TYPE.STRING],
8982- isRequired: false,
89838985 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
89848986 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIsUNIQUE_APIS.findIndex(x => x === selected)),
89858987 selected[0].toUpperCase() ?? enumIcons.default)),
89868988 }),
89878989 ],
@@ -10655,6 +10657,15 @@ jQuery(async function () {
1065510657 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
1065610658 /* $('#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+
1065810669 $(document).keyup(function (e) {
1065910670 if (e.key === 'Escape') {
1066010671 const isEditVisible = $('#curEditTextarea').is(':visible');
@@ -10738,7 +10749,7 @@ jQuery(async function () {
1073810749 }
1073910750 } break;
1074010751 case 'import_tags': {
1074110752 await importTags(characters[this_chid], { forceShowimportSetting: truetag_import_setting.ASK });
1074210753 } break;
1074310754 /*case 'delete_button':
1074410755 popup_type = "del_ch";
public/scripts/BulkEditOverlay.js+33 -3
@@ -18,7 +18,7 @@ import {
1818import { favsToHotswap } from './RossAscends-mods.js';
1919import { hideLoader, showLoader } from './loader.js';
2020import { convertCharacterToPersona } from './personas.js';
2121import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
2222
2323/**
2424 * Static object representing the actions of the
@@ -197,10 +197,10 @@ class BulkTagPopupHandler {
197197 #getHtml = () => {
198198 const characterData = JSON.stringify({ characterIds: this.characterIds });
199199 return `<div id="bulk_tag_shadow_popup">
200200 <div id="bulk_tag_popup" class="wider_dialogue_popup">
201201 <div id="bulk_tag_popup_holder">
202202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
203203 <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>
204204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
205205 <br>
206206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
@@ -219,6 +219,12 @@ class BulkTagPopupHandler {
219219 <i class="fa-solid fa-trash-can margin-right-10px"></i>
220220 Mutual
221221 </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>
222228 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
223229 </div>
224230 </div>
@@ -254,6 +260,30 @@ class BulkTagPopupHandler {
254260 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
255261 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
256262 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();
257287 }
258288
259289 /**
public/scripts/RossAscends-mods.js+5 -4
@@ -954,6 +954,11 @@ export function initRossMods() {
954954 * @param {KeyboardEvent} event
955955 */
956956 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+
957962 //Enter to send when send_textarea in focus
958963 if (document.activeElement == hotkeyTargets['send_textarea']) {
959964 const sendOnEnter = shouldSendOnEnter();
@@ -1107,10 +1112,6 @@ export function initRossMods() {
11071112 }
11081113
11091114 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-
11141115 //dont override Escape hotkey functions from script.js
11151116 //"close edit box" and "cancel stream generation".
11161117 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {
public/scripts/extensions/caption/settings.html+1 -0
@@ -38,6 +38,7 @@
3838 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
3939 <option data-type="openai" value="gpt-4o">gpt-4o</option>
4040 <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>
4142 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
4243 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
4344 <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) {
401401 const dataBankCollectionIds = await ingestDataBankAttachments();
402402
403403 if (dataBankCollectionIds.length) {
404404 const queryText = await getQueryText(chat, 'file');
405405 await injectDataBankChunks(queryText, dataBankCollectionIds);
406406 }
407407
@@ -435,7 +435,7 @@ async function processFiles(chat) {
435435 await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size, settings.overlap_percent);
436436 }
437437
438438 const queryText = await getQueryText(chat, 'file');
439439 const fileChunks = await retrieveFileChunks(queryText, collectionId);
440440
441441 message.mes = `${fileChunks}\n\n${message.mes}`;
@@ -596,7 +596,7 @@ async function rearrangeChat(chat) {
596596 return;
597597 }
598598
599599 const queryText = await getQueryText(chat, 'chat');
600600
601601 if (queryText.length === 0) {
602602 console.debug('Vectors: No text to query');
@@ -683,15 +683,16 @@ const onChatEvent = debounce(async () => await moduleWorker.update(), debounce_t
683683/**
684684 * Gets the text to query from the chat
685685 * @param {object[]} chat Chat messages
686+ * @param {'file'|'chat'|'world-info'} initiator Initiator of the query
686687 * @returns {Promise<string>} Text to query
687688 */
688689async function getQueryText(chat, initiator) {
689690 let queryText = '';
690691 let i = 0;
691692
692693 let hashedMessages = chat.map(x => ({ text: String(substituteParams(x.mes)) }));
693694
694695 if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) {
695696 hashedMessages = await summarize(hashedMessages, settings.summary_source);
696697 }
697698
@@ -1279,7 +1280,7 @@ async function activateWorldInfo(chat) {
12791280 }
12801281
12811282 // Perform a multi-query
12821283 const queryText = await getQueryText(chat, 'world-info');
12831284
12841285 if (queryText.length === 0) {
12851286 console.debug('Vectors: No text to query for WI');
public/scripts/openai.js+2 -1
@@ -4763,12 +4763,13 @@ export function isImageInliningSupported() {
47634763 'gpt-4-turbo',
47644764 'gpt-4o',
47654765 'gpt-4o-mini',
4766+ 'chatgpt-4o-latest',
47664767 'yi-vision',
47674768 ];
47684769
47694770 switch (oai_settings.chat_completion_source) {
47704771 case chat_completion_sources.OPENAI:
47714772 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'));
47724773 case chat_completion_sources.MAKERSUITE:
47734774 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
47744775 case chat_completion_sources.CLAUDE:
public/scripts/popup.js+1 -1
@@ -596,7 +596,7 @@ export class Popup {
596596
597597 /** @returns {boolean} Checks if any modal popup dialog is open */
598598 isPopupOpen() {
599599 return Popup.util.popups.filter(x => x.dlg.hasAttribute('open')).length > 0;
600600 },
601601
602602 /**
public/scripts/power-user.js+8 -0
@@ -202,6 +202,7 @@ let power_user = {
202202 trim_spaces: true,
203203 relaxed_api_urls: false,
204204 world_import_dialog: true,
205+ enable_auto_select_input: false,
205206 tag_import_setting: tag_import_setting.ASK,
206207 disable_group_trimming: false,
207208 single_line: false,
@@ -1611,6 +1612,7 @@ async function loadPowerUserSettings(settings, data) {
16111612 $('#single_line').prop('checked', power_user.single_line);
16121613 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
16131614 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
1615+ $('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);
16141616 $('#trim_spaces').prop('checked', power_user.trim_spaces);
16151617 $('#continue_on_send').prop('checked', power_user.continue_on_send);
16161618 $('#quick_continue').prop('checked', power_user.quick_continue);
@@ -3788,6 +3790,12 @@ $(document).ready(() => {
37883790 saveSettingsDebounced();
37893791 });
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+
37913799 $('#spoiler_free_mode').on('input', function () {
37923800 power_user.spoiler_free_mode = !!$(this).prop('checked');
37933801 switchSpoilerMode();
public/scripts/showdown-underscore.js+13 -3
@@ -7,9 +7,19 @@ export const markdownUnderscoreExt = () => {
77 }
88
99 return [{
1010 type: 'langoutput',
1111 regex: new RegExp('(<code>[\\bs\\S]*?<\\/code>)|(?<!_\\S)_(?!_)(.*[^_\\n]+?)(?<!_)_(?!_)\\bw)', '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+ },
1323 }];
1424 } catch (e) {
1525 console.error('Error in Showdown-underscore extension:', e);
public/scripts/slash-commands.js+211 -24
@@ -1,7 +1,9 @@
11import {
22 Generate,
3+ UNIQUE_APIS,
34 activateSendButtons,
45 addOneMessage,
6+ api_server,
57 callPopup,
68 characters,
79 chat,
@@ -49,8 +51,8 @@ import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSel
4951import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
5052import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
5153import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5254import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
5355import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
5456import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
5557import { registerVariableCommands, resolveVariable } from './variables.js';
5658import { background_settings } from './backgrounds.js';
@@ -1496,7 +1498,8 @@ export function initDefaultSlashCommands() {
14961498 ],
14971499 helpString: 'Sets the specified prompt manager entry/entries on or off.',
14981500 }));
14991501 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',
1502+ name: 'pick-icon',
15001503 callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
15011504 returns: 'The chosen icon name or false if cancelled.',
15021505 helpString: `
@@ -1511,6 +1514,72 @@ export function initDefaultSlashCommands() {
15111514 </div>
15121515 `,
15131516 }));
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
15151584 registerVariableCommands();
15161585}
@@ -1788,7 +1857,7 @@ async function popupCallback(args, value) {
17881857 return String(value);
17891858}
17901859
17911860async function getMessagesCallback(args, value) {
17921861 const includeNames = !isFalseBoolean(args?.names);
17931862 const includeHidden = isTrueBoolean(args?.hidden);
17941863 const role = args?.role;
@@ -1821,33 +1890,34 @@ function getMessagesCallback(args, value) {
18211890 throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
18221891 };
18231892
1824- const messages = [];
1893+ const processMessage = async (mesId) => {
1825-
1894+ 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;
18311898 }
18321899
18331900 if (role && !filterByRole(messagemsg)) {
18341901 console.debug(`/messages: Skipping message with ID ${messageIdmesId} due to role filter`);
18351902 continuereturn null;
18361903 }
18371904
18381905 if (!includeHidden && messagemsg.is_system) {
18391906 console.debug(`/messages: Skipping hidden message with ID ${messageIdmesId}`);
18401907 continuereturn null;
18411908 }
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- }
1848- }
18491914
1850- return messages.join('\n\n');
1915+ for (let rInd = range.start; rInd <= range.end; ++rInd)
1916+ messagePromises.push(processMessage(rInd));
1917+
1918+ const messages = await Promise.all(messagePromises);
1919+
1920+ return messages.filter(m => m !== null).join('\n\n');
18511921}
18521922
18531923async function runCallback(args, name) {
@@ -3418,6 +3488,123 @@ function setPromptEntryCallback(args, targetState) {
34183488 return '';
34193489}
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+ */
3500+async 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+
3587+async 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+
34213608export let isExecutingCommandsFromChatInput = false;
34223609export let commandsFromChatInputAbortController;
34233610
public/scripts/tags.js+8 -8
@@ -708,12 +708,12 @@ const ANTI_TROLL_MAX_TAGS = 15;
708708 *
709709 * @param {Character} character - The character
710710 * @param {object} [options] - Options
711711 * @param {booleantag_import_setting} [options.forceShowimportSetting=falsenull] - Whether to forceForce showinga thetag import dialogsetting
712712 * @returns {Promise<boolean>} Boolean indicating whether any tag was imported
713713 */
714714async function importTags(character, { forceShowimportSetting = falsenull } = {}) {
715715 // Gather the tags to import based on the selected setting
716716 const tagNamesToImport = await handleTagImport(character, { forceShowimportSetting });
717717 if (!tagNamesToImport?.length) {
718718 console.debug('No tags to import');
719719 return;
@@ -732,10 +732,10 @@ async function importTags(character, { forceShow = false } = {}) {
732732 *
733733 * @param {Character} character - The character
734734 * @param {object} [options] - Options
735735 * @param {booleantag_import_setting} [options.forceShowimportSetting=falsenull] - Whether to forceForce showinga thetag import dialogsetting
736736 * @returns {Promise<string[]>} Array of strings representing the tags to import
737737 */
738738async function handleTagImport(character, { forceShowimportSetting = falsenull } = {}) {
739739 /** @type {string[]} */
740740 const importTags = character.tags.map(t => t.trim()).filter(t => t)
741741 .filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))
@@ -745,9 +745,9 @@ async function handleTagImport(character, { forceShow = false } = {}) {
745745 .map(newTag);
746746 const folderTags = getOpenBogusFolders();
747747
748748 // Choose the setting for this dialog. IfFirst fromcheck settingsoverride, verifythen thesaved setting really exists,or otherwisefinally takeuse "ASK".
749749 const setting = forceShowimportSetting ? tag_import_setting.ASKimportSetting :
750750 : Object.values(tag_import_setting).find(setting => setting === power_user.tag_import_setting) ?? tag_import_setting.ASK;
751751
752752 switch (setting) {
753753 case tag_import_setting.ALL:
public/scripts/textgen-settings.js+30 -2
@@ -95,7 +95,7 @@ let DREAMGEN_SERVER = 'https://dreamgen.com';
9595let OPENROUTER_SERVER = 'https://openrouter.ai/api';
9696let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';
9797
9898export const SERVER_INPUTS = {
9999 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
100100 [textgen_types.VLLM]: '#vllm_api_url_text',
101101 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',
@@ -1071,6 +1071,34 @@ function getLogprobsNumber() {
10711071 return 10;
10721072}
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+ */
1079+function 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+
10741102export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
10751103 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
10761104 const dynatemp = isDynamicTemperatureSupported();
@@ -1110,7 +1138,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
11101138 'dry_allowed_length': settings.dry_allowed_length,
11111139 'dry_multiplier': settings.dry_multiplier,
11121140 'dry_base': settings.dry_base,
11131141 'dry_sequence_breakers': replaceMacrosInList(settings.dry_sequence_breakers),
11141142 'dry_penalty_last_n': settings.dry_penalty_last_n,
11151143 'max_tokens_second': settings.max_tokens_second,
11161144 'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined,
public/scripts/tokenizers.js+40 -2
@@ -148,9 +148,45 @@ async function resetTokenCache() {
148148}
149149
150150/**
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+ */
161+export 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+ */
174+export 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+/**
151187 * Gets the friendly name of the current tokenizer.
152188 * @param {string} forApi API to get the tokenizer for. Defaults to the main API.
153189 * @returns { { tokenizerName: string, tokenizerId: number } Tokenizer} Tokenizer info
154190 */
155191export function getFriendlyTokenizerName(forApi) {
156192 if (!forApi) {
@@ -185,7 +221,9 @@ export function getFriendlyTokenizerName(forApi) {
185221 ? tokenizers.OPENAI
186222 : tokenizerId;
187223
188- return { tokenizerName, tokenizerId };
224+ const tokenizerKey = Object.entries(tokenizers).find(([_, value]) => value === tokenizerId)[0].toLocaleLowerCase();
225+
226+ return { tokenizerName, tokenizerKey, tokenizerId };
189227}
190228
191229/**
server.js+7 -9
@@ -787,17 +787,17 @@ function logSecurityAlert(message) {
787787 */
788788function handleServerListenFail(v6Failed, v4Failed) {
789789 if (v6Failed && !enableIPv4) {
790790 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
791791 process.exit(1);
792792 }
793793
794794 if (v4Failed && !enableIPv6) {
795795 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
796796 process.exit(1);
797797 }
798798
799799 if (v6Failed && v4Failed) {
800800 console.error(color.red('fatal error: Failed to start server on both IPv6 and IPv4'));
801801 process.exit(1);
802802 }
803803}
@@ -846,9 +846,8 @@ async function startHTTPorHTTPS() {
846846 try {
847847 await createFunc(tavernUrlV6);
848848 } 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
853852 v6Failed = true;
854853 }
@@ -858,9 +857,8 @@ async function startHTTPorHTTPS() {
858857 try {
859858 await createFunc(tavernUrl);
860859 } 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
865863 v4Failed = true;
866864 }
src/endpoints/translate.js+6 -3
@@ -1,6 +1,7 @@
11const fetch = require('node-fetch').default;
22const https = require('https');
33const express = require('express');
4+const iconv = require('iconv-lite');
45const { readSecret, SECRET_KEYS } = require('./secrets');
56const { getConfigValue, uuidv4 } = require('../util');
67const { jsonParser } = require('../express-common');
@@ -80,16 +81,18 @@ router.post('/google', jsonParser, async (request, response) => {
8081 const url = generateRequestUrl(text, { to: lang });
8182
8283 https.get(url, (resp) => {
8384 letconst data = ''[];
8485
8586 resp.on('data', (chunk) => {
8687 data += .push(chunk);
8788 });
8889
8990 resp.on('end', () => {
9091 try {
9192 const resultdecodedData = normaliseResponseiconv.decode(JSONBuffer.parseconcat(data), 'utf-8');
93+ const result = normaliseResponse(JSON.parse(decodedData));
9294 console.log('Translated text: ' + result.text);
95+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
9396 return response.send(result.text);
9497 } catch (error) {
9598 console.log('Translation error', error);
src/transformers.mjs+36 -3
@@ -1,6 +1,7 @@
11import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';
22import { getConfigValue } from './util.js';
33import path from 'path';
4+import fs from 'fs';
45
56configureTransformers();
67
@@ -48,7 +49,7 @@ const tasks = {
4849 configField: 'extras.textToSpeechModel',
4950 quantized: false,
5051 },
5152};
5253
5354/**
5455 * Gets a RawImage object from a base64-encoded image.
@@ -85,6 +86,36 @@ function getModelForTask(task) {
8586 }
8687}
8788
89+async 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+
88119/**
89120 * Gets the transformers.js pipeline for a given task.
90121 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for
@@ -92,6 +123,8 @@ function getModelForTask(task) {
92123 * @returns {Promise<Pipeline>} Pipeline for the task
93124 */
94125async function getPipeline(task, forceModel = '') {
126+ await migrateCacheToDataDir();
127+
95128 if (tasks[task].pipeline) {
96129 if (forceModel === '' || tasks[task].currentModel === forceModel) {
97130 return tasks[task].pipeline;
@@ -100,11 +133,11 @@ async function getPipeline(task, forceModel = '') {
100133 await tasks[task].pipeline.dispose();
101134 }
102135
103136 const cache_dircacheDir = path.join(processglobal.cwd()DATA_ROOT, 'cache_cache');
104137 const model = forceModel || getModelForTask(task);
105138 const localOnly = getConfigValue('extras.disableAutoDownload', false);
106139 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
107140 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
108141 tasks[task].pipeline = instance;
109142 tasks[task].currentModel = model;
110143 return instance;
src/users.js+5 -11
@@ -20,12 +20,6 @@ const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
2020const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2121
2222/**
23- * The root directory for user data.
24- * @type {string}
25- */
26-let DATA_ROOT = './data';
27-
28-/**
2923 * Cache for user directories.
3024 * @type {Map<string, UserDirectoryList>}
3125 */
@@ -138,7 +132,7 @@ async function migrateUserData() {
138132
139133 console.log();
140134 console.log(color.magenta('Preparing to migrate user data...'));
141135 console.log(`All public data will be moved to the ${global.DATA_ROOT} directory.`);
142136 console.log('This process may take a while depending on the amount of data to move.');
143137 console.log(`Backups will be placed in the ${PUBLIC_DIRECTORIES.backups} directory.`);
144138 console.log(`The process will start in ${TIMEOUT} seconds. Press Ctrl+C to cancel.`);
@@ -352,11 +346,11 @@ function toAvatarKey(handle) {
352346 * @returns {Promise<void>}
353347 */
354348async function initUserStorage(dataRoot) {
355349 global.DATA_ROOT = dataRoot;
356350 console.log('Using data root:', color.green(global.DATA_ROOT));
357351 console.log();
358352 await storage.init({
359353 dir: path.join(global.DATA_ROOT, '_storage'),
360354 ttl: false, // Never expire
361355 });
362356
@@ -457,7 +451,7 @@ function getUserDirectories(handle) {
457451
458452 const directories = structuredClone(USER_DIRECTORY_TEMPLATE);
459453 for (const key in directories) {
460454 directories[key] = path.join(global.DATA_ROOT, handle, USER_DIRECTORY_TEMPLATE[key]);
461455 }
462456 DIRECTORIES_CACHE.set(handle, directories);
463457 return directories;