Merge remote-tracking branch 'alpin/staging' into sampler_order

a1115ab4ffc15706ff1a055822b662520fd259ae

AlpinDale <alpindale@gmail.com>

32 files changed, +648 -325Ignore whitespace
.eslintrc.cjs+1 -0
@@ -54,6 +54,7 @@ module.exports = {
5454 },
5555 // These scripts are loaded in HTML; tell ESLint not to complain about them being undefined
5656 globals: {
57+ globalThis: 'readonly',
5758 ePub: 'readonly',
5859 pdfjsLib: 'readonly',
5960 toastr: 'readonly',
.github/readme-ru_ru.md+38 -0
@@ -209,6 +209,44 @@ SillyTavern поддерживает расширения.
2092095. Запустите лаунчер установки: `chmod +x install.sh && ./install.sh` and choose what you wanna install
2102106. После завершения установки, запустите лаунчер следующей командой: `chmod +x launcher.sh && ./launcher.sh`
211211
212+## 🐋 Установка с помощью Docker
213+
214+Предполагается, что вы уже установили Docker, имеете доступ к командной строке для установки контейнеров и знакомы с их базовым управлением.
215+
216+### Сборка образа самостоятельно
217+
218+У нас есть подробное руководство по использованию SillyTavern в Docker [здесь](http://docs.sillytavern.app/installation/docker/), которое охватывает установку на Windows, macOS и Linux! Ознакомьтесь с ним, если хотите создать образ самостоятельно.
219+
220+### Использование реестра контейнеров GitHub (самый простой способ)
221+
222+Для работы SillyTavern вам понадобятся две обязательные настройки каталогов и одна настройка порта. В команде замените указанные значения на свои:
223+
224+#### Переменные контейнера
225+
226+##### Маппинг томов
227+
228+* [config] - директория, где на вашем хосте будут храниться файлы конфигурации SillyTavern.
229+* [data] - директория, где на вашем хосте будут храниться пользовательские данные SillyTavern (включая персонажей).
230+* [plugins] - (необязательно) директория, где на вашем хосте будут храниться плагины сервера SillyTavern.
231+
232+##### Маппинг портов
233+
234+* [PublicPort] - Порт, через который будет передаваться трафик. Это обязательно, так как вы будете обращаться к контейнеру извне его виртуальной машины. НЕ ОТКРЫВАЙТЕ этот порт в интернет без реализации дополнительного уровня безопасности.
235+
236+##### Дополнительные настройки
237+
238+* [DockerNet] - Docker сеть, к которой контейнер должен быть подключен. Если вы не знаете, что это, обратитесь к [официальной документации Docker](https://docs.docker.com/reference/cli/docker/network/).
239+* [version] - на правой части этой страницы GitHub вы найдете раздел "Packages". Выберите пакет "sillytavern", чтобы увидеть версии образов. Тег "latest" позволит вам обновляться до текущего релиза. Также доступны теги "staging" и "release", которые соответствуют ночным сборкам соответствующих веток. Однако это может быть нецелесообразно, если вы используете расширения, которые могут ломаться и требуют времени для обновления.
240+
241+#### Команда установки
242+
243+1. Откройте командную строку
244+2. Выполните следующую команду
245+
246+`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
247+
248+> Заметьте, что 8000 является портом по умолчанию. Не забудьте использовать соответствующий порт, если вы измените его в конфиге.
249+
212250## 📱 Мобильные устройства - Установка при помощи termux
213251
214252> **ОБРАТИТЕ ВНИМАНИЕ!**
default/config.yaml+2 -0
@@ -1,6 +1,8 @@
11# -- DATA CONFIGURATION --
22# Root directory for user data storage
33dataRoot: ./data
4+# The maximum amount of memory that parsed character cards can use in MB
5+cardsCacheCapacity: 100
46# -- SERVER CONFIGURATION --
57# Listen for incoming connections
68listen: false
package-lock.json+11 -7
@@ -3591,9 +3591,9 @@
35913591 "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="
35923592 },
35933593 "node_modules/express": {
35943594 "version": "4.21.12",
35953595 "resolved": "https://registry.npmjs.org/express/-/express-4.21.12.tgz",
35963596 "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+BwuM1IPw8fmQBiQrXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
35973597 "license": "MIT",
35983598 "dependencies": {
35993599 "accepts": "~1.3.8",
@@ -3615,7 +3615,7 @@
36153615 "methods": "~1.1.2",
36163616 "on-finished": "2.4.1",
36173617 "parseurl": "~1.3.3",
36183618 "path-to-regexp": "0.1.1012",
36193619 "proxy-addr": "~2.0.7",
36203620 "qs": "6.13.0",
36213621 "range-parser": "~1.2.1",
@@ -3630,6 +3630,10 @@
36303630 },
36313631 "engines": {
36323632 "node": ">= 0.10.0"
3633+ },
3634+ "funding": {
3635+ "type": "opencollective",
3636+ "url": "https://opencollective.com/express"
36333637 }
36343638 },
36353639 "node_modules/express/node_modules/cookie": {
@@ -5725,9 +5729,9 @@
57255729 }
57265730 },
57275731 "node_modules/path-to-regexp": {
57285732 "version": "0.1.1012",
57295733 "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.1012.tgz",
57305734 "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfaRA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/BSsIwjuh56VU7katFvuM8hULfkwB3FnsTt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/rsVF7PwPKVw1sl5KQS9wbSeOZwt1pmEQ==",
57315735 "license": "MIT"
57325736 },
57335737 "node_modules/peek-readable": {
public/global.d.ts+2 -1
@@ -1,4 +1,5 @@
11import libs from './lib';
2+import getContext from './scripts/st-context';
23
34// Global namespace modules
45declare var ai;
@@ -6,7 +7,7 @@ declare var pdfjsLib;
67declare var ePub;
78
89declare var SillyTavern: {
910 getContext(): anytypeof getContext;
1011 llm: any;
1112 libs: typeof libs;
1213};
public/index.html+8 -7
@@ -669,7 +669,7 @@
669669 </span>
670670 </div>
671671 </div>
672672 <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt">
673673 <div class="range-block-title" data-i18n="Temperature">
674674 Temperature
675675 </div>
@@ -682,7 +682,7 @@
682682 </div>
683683 </div>
684684 </div>
685685 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt">
686686 <div class="range-block-title" data-i18n="Frequency Penalty">
687687 Frequency Penalty
688688 </div>
@@ -695,7 +695,7 @@
695695 </div>
696696 </div>
697697 </div>
698698 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt">
699699 <div class="range-block-title" data-i18n="Presence Penalty">
700700 Presence Penalty
701701 </div>
@@ -721,7 +721,7 @@
721721 </div>
722722 </div>
723723 </div>
724724 <div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt">
725725 <div class="range-block-title" data-i18n="Top P">
726726 Top P
727727 </div>
@@ -958,7 +958,7 @@
958958 </div>
959959 </div>
960960 </div>
961961 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt">
962962 <div class="range-block-title justifyLeft" data-i18n="Seed">
963963 Seed
964964 </div>
@@ -1425,7 +1425,7 @@
14251425 <input class="neo-range-slider" type="range" id="dry_allowed_length_textgenerationwebui" min="1" max="20" step="1" />
14261426 <input class="neo-range-input" type="number" min="1" max="20" step="1" data-for="dry_allowed_length_textgenerationwebui" id="dry_allowed_length_counter_textgenerationwebui">
14271427 </div>
14281428 <div class="alignItemsCenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0" data-tg-type="llamacpp, koboldcpp, tabby, aphrodite">
14291429 <small data-i18n="Penalty Range">Penalty Range</small>
14301430 <input class="neo-range-slider" type="range" id="dry_penalty_last_n_textgenerationwebui" min="0" max="8192" step="1" />
14311431 <input class="neo-range-input" type="number" min="0" max="8192" step="1" data-for="dry_penalty_last_n_textgenerationwebui" id="dry_penalty_last_n_counter_textgenerationwebui">
@@ -3016,6 +3016,7 @@
30163016 <optgroup label="Subversions">
30173017 <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>
30183018 <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>
3019+ <option value="gemini-exp-1206">Gemini Experimental 2024-12-06</option>
30193020 <option value="gemini-1.5-pro-exp-0801">Gemini 1.5 Pro Experiment 2024-08-01</option>
30203021 <option value="gemini-1.5-pro-exp-0827">Gemini 1.5 Pro Experiment 2024-08-27</option>
30213022 <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>
@@ -4212,7 +4213,7 @@
42124213 </label>
42134214 <label for="show_swipe_num_all_messages" class="checkbox_label" title="Display swipe numbers for all messages, not just the last." data-i18n="[title]Display swipe numbers for all messages, not just the last.">
42144215 <input id="show_swipe_num_all_messages" type="checkbox" />
42154216 <small data-i18n="Swipe # for All Messages">Swipe # for All Messages</small><i class="fa-solid fa-mobile-screen-button"></i>
42164217 </label>
42174218 <label for="hotswapEnabled" class="checkbox_label" title="In the Character Management panel, show quick selection buttons for favorited characters." data-i18n="[title]In the Character Management panel, show quick selection buttons for favorited characters">
42184219 <input id="hotswapEnabled" type="checkbox" />
public/jsconfig.json+2 -1
@@ -5,7 +5,8 @@
55 "module": "ESNext",
66 "moduleResolution": "node",
77 "allowUmdGlobalAccess": true,
88 "allowSyntheticDefaultImports": true,
9+ "strictBindCallApply": true
910 },
1011 "exclude": [
1112 "**/node_modules/**",
public/script.js+75 -163
@@ -13,7 +13,7 @@ import {
1313 default as libs,
1414} from './lib.js';
1515
1616import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods, shouldSendOnEnter } from './scripts/RossAscends-mods.js';
1717import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js';
1818import {
1919 generateKoboldWithStreaming,
@@ -37,8 +37,6 @@ import {
3737 parseTabbyLogprobs,
3838} from './scripts/textgen-settings.js';
3939
40-const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER, FEATHERLESS } = textgen_types;
41-
4240import {
4341 world_info,
4442 getWorldInfoPrompt,
@@ -67,9 +65,7 @@ import {
6765 getGroupChat,
6866 renameGroupMember,
6967 createNewGroupChat,
70- getGroupPastChats,
7168 getGroupAvatar,
72- openGroupChat,
7369 editGroup,
7470 deleteGroupChat,
7571 renameGroupChat,
@@ -98,7 +94,6 @@ import {
9894 resetMovableStyles,
9995 forceCharacterEditorTokenize,
10096 applyPowerUserSettings,
101- switchSwipeNumAllMessages,
10297} from './scripts/power-user.js';
10398
10499import {
@@ -174,8 +169,8 @@ import {
174169} from './scripts/utils.js';
175170import { debounce_timeout } from './scripts/constants.js';
176171
177172import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, initExtensions, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
178173import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
179174import {
180175 tag_map,
181176 tags,
@@ -225,8 +220,8 @@ import {
225220 instruct_presets,
226221 selectContextPreset,
227222} from './scripts/instruct-mode.js';
228223import { initLocales, t, translate } from './scripts/i18n.js';
229224import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, getTokenizerModel, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js';
230225import {
231226 user_avatar,
232227 getUserAvatars,
@@ -241,12 +236,12 @@ import { hideLoader, showLoader } from './scripts/loader.js';
241236import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
242237import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels } from './scripts/textgen-models.js';
243238import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js';
244239import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
245240import { MacrosParser, evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
246241import { currentUser, setUserControls } from './scripts/user.js';
247242import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
248243import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
249244import { initScrapers, ScraperManager } from './scripts/scrapers.js';
250245import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
251246import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
252247import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -268,6 +263,13 @@ import { initServerHistory } from './scripts/server-history.js';
268263import { initSettingsSearch } from './scripts/setting-search.js';
269264import { initBulkEdit } from './scripts/bulk-edit.js';
270265import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
266+import { getContext } from './scripts/st-context.js';
267+
268+// API OBJECT FOR EXTERNAL WIRING
269+globalThis.SillyTavern = {
270+ libs,
271+ getContext,
272+};
271273
272274//exporting functions and vars for mods
273275export {
@@ -427,10 +429,6 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
427429 }
428430});
429431
430-// API OBJECT FOR EXTERNAL WIRING
431-window['SillyTavern'] = {};
432-window['SillyTavern'].libs = libs;
433-
434432// Event source init
435433export const event_types = {
436434 APP_READY: 'app_ready',
@@ -536,7 +534,7 @@ let displayVersion = 'SillyTavern';
536534
537535let generatedPromptCache = '';
538536let generation_started = new Date();
539537/** @type {import('./scripts/char-data.js').v1CharData[]} */
540538export let characters = [];
541539export let this_chid;
542540let saveCharactersPage = 0;
@@ -812,7 +810,7 @@ export let menu_type = '';
812810export let selected_button = ''; //which button pressed
813811
814812//create pole save
815813export let create_save = {
816814 name: '',
817815 description: '',
818816 creator_notes: '',
@@ -864,7 +862,7 @@ export let amount_gen = 80; //default max length of AI generated responses
864862export let max_context = 2048;
865863
866864var swipes = true;
867865export let extension_prompts = {};
868866
869867export let main_api;// = "kobold";
870868//novel settings
@@ -1175,7 +1173,7 @@ async function getStatusTextgen() {
11751173 return resultCheckStatus();
11761174 }
11771175
11781176 if (textgen_settings.type == textgen_types.OOBA && textgen_settings.bypass_status_check) {
11791177 setOnlineStatus('Status check bypassed');
11801178 return resultCheckStatus();
11811179 }
@@ -1193,34 +1191,34 @@ async function getStatusTextgen() {
11931191
11941192 const data = await response.json();
11951193
11961194 if (textgen_settings.type === textgen_types.MANCER) {
11971195 loadMancerModels(data?.data);
11981196 setOnlineStatus(textgen_settings.mancer_model);
11991197 } else if (textgen_settings.type === textgen_types.TOGETHERAI) {
12001198 loadTogetherAIModels(data?.data);
12011199 setOnlineStatus(textgen_settings.togetherai_model);
12021200 } else if (textgen_settings.type === textgen_types.OLLAMA) {
12031201 loadOllamaModels(data?.data);
12041202 setOnlineStatus(textgen_settings.ollama_model || 'Connected');
12051203 } else if (textgen_settings.type === textgen_types.INFERMATICAI) {
12061204 loadInfermaticAIModels(data?.data);
12071205 setOnlineStatus(textgen_settings.infermaticai_model);
12081206 } else if (textgen_settings.type === textgen_types.DREAMGEN) {
12091207 loadDreamGenModels(data?.data);
12101208 setOnlineStatus(textgen_settings.dreamgen_model);
12111209 } else if (textgen_settings.type === textgen_types.OPENROUTER) {
12121210 loadOpenRouterModels(data?.data);
12131211 setOnlineStatus(textgen_settings.openrouter_model);
12141212 } else if (textgen_settings.type === textgen_types.VLLM) {
12151213 loadVllmModels(data?.data);
12161214 setOnlineStatus(textgen_settings.vllm_model);
12171215 } else if (textgen_settings.type === textgen_types.APHRODITE) {
12181216 loadAphroditeModels(data?.data);
12191217 setOnlineStatus(textgen_settings.aphrodite_model);
12201218 } else if (textgen_settings.type === textgen_types.FEATHERLESS) {
12211219 loadFeatherlessModels(data?.data);
12221220 setOnlineStatus(textgen_settings.featherless_model);
12231221 } else if (textgen_settings.type === textgen_types.TABBY) {
12241222 loadTabbyModels(data?.data);
12251223 setOnlineStatus(textgen_settings.tabby_model || data?.result);
12261224 } else {
@@ -1637,7 +1635,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
16371635 subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false, tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false });
16381636 }
16391637 if (doSort) {
16401638 sortEntitiesList(subEntities, false);
16411639 }
16421640 entity.entities = subEntities;
16431641 entity.hidden = subCount - subEntities.length;
@@ -1665,7 +1663,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
16651663
16661664 // Sort before returning if requested
16671665 if (doSort) {
16681666 sortEntitiesList(entities, false);
16691667 }
16701668 entitiesFilter.clearFuzzySearchCaches();
16711669 return entities;
@@ -2607,15 +2605,18 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
26072605 };
26082606 }
26092607
26102608 const getGroupValue = (includeMuted) => {
26112609 if (typeof _group === 'string') {
26122610 return _group;
26132611 }
26142612
26152613 if (selected_group) {
26162614 const members = groups.find(x => x.id === selected_group)?.members;
2615+ /** @type {string[]} */
2616+ const disabledMembers = groups.find(x => x.id === selected_group)?.disabled_members ?? [];
2617+ const isMuted = x => includeMuted ? true : !disabledMembers.includes(x);
26172618 const names = Array.isArray(members)
26182619 ? members.filter(isMuted).map(m => characters.find(c => c.avatar === m)?.name).filter(Boolean).join(', ')
26192620 : '';
26202621 return names;
26212622 } else {
@@ -2639,7 +2640,8 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
26392640 // Must be substituted last so that they're replaced inside {{description}}
26402641 environment.user = _name1 ?? name1;
26412642 environment.char = _name2 ?? name2;
26422643 environment.group = environment.charIfNotGroup = getGroupValue(true);
2644+ environment.groupNotMuted = getGroupValue(false);
26432645 environment.model = getGeneratingModel();
26442646
26452647 if (additionalMacro && typeof additionalMacro === 'object') {
@@ -4494,6 +4496,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44944496 instruction: main_api !== 'openai' && power_user.sysprompt.enabled ? substituteParams(power_user.prefer_character_prompt && system ? system : power_user.sysprompt.content) : '',
44954497 userPersona: (power_user.persona_description_position == persona_description_positions.IN_PROMPT ? (persona || '') : ''),
44964498 tokenizer: getFriendlyTokenizerName(main_api).tokenizerName || '',
4499+ presetName: getPresetManager()?.getSelectedPresetName() || '',
44974500 };
44984501
44994502 //console.log(additionalPromptStuff);
@@ -5160,6 +5163,7 @@ export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMes
51605163 dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),
51615164 modelUsed: chat[incomingMesId]?.extra?.model,
51625165 apiUsed: chat[incomingMesId]?.extra?.api,
5166+ presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
51635167 };
51645168
51655169 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
@@ -5536,7 +5540,7 @@ function extractMultiSwipes(data, type) {
55365540 return swipes;
55375541 }
55385542
55395543 if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [textgen_types.MANCER, textgen_types.VLLM, textgen_types.APHRODITE, textgen_types.TABBY, textgen_types.INFERMATICAI].includes(textgen_settings.type))) {
55405544 if (!Array.isArray(data.choices)) {
55415545 return swipes;
55425546 }
@@ -5699,7 +5703,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
56995703 return getMessage;
57005704}
57015705
57025706export async function saveReply(type, getMessage, fromStreaming, title, swipes) {
57035707 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
57045708 chat[chat.length - 1]['is_user'])) {
57055709 type = 'normal';
@@ -7658,14 +7662,12 @@ export function showSwipeButtons() {
76587662 //allows for writing individual swipe counters for past messages
76597663 const lastSwipeCounter = $('.last_mes .swipes-counter');
76607664 lastSwipeCounter.text(swipeCounterText).show();
7661-
7662- switchSwipeNumAllMessages();
76637665}
76647666
76657667export function hideSwipeButtons() {
76667668 $('#chat')chatElement.find('.swipe_right').hide();
76677669 $('#chat')chatElement.find('.last_mes .swipes-counter').hide();
76687670 $('#chat')chatElement.find('.swipe_left').hide();
76697671}
76707672
76717673/**
@@ -7752,25 +7754,31 @@ export async function saveChatConditional() {
77527754 }
77537755}
77547756
7755-async function importCharacterChat(formData) {
7757+/**
7756- await jQuery.ajax({
7758+ * Saves the chat to the server.
7757- type: 'POST',
7759+ * @param {FormData} formData Form data to send to the server.
7758- url: '/api/chats/import',
7760+ * @param {EventTarget} eventTarget Event target to trigger the event on.
7759- data: formData,
7761+ */
77607762 beforeSend:async function importCharacterChat(formData, eventTarget) {
7761- },
7763+ const headers = getRequestHeaders();
7762- cache: false,
7764+ delete headers['Content-Type'];
7763- contentType: false,
7765+ const fetchResult = await fetch('/api/chats/import', {
77647766 processDatamethod: false'POST',
7765- success: async function (data) {
7767+ body: formData,
7766- if (data.res) {
7768+ headers: headers,
7767- await displayPastChats();
7769+ cache: 'no-cache',
7768- }
7769- },
7770- error: function () {
7771- $('#create_button').removeAttr('disabled');
7772- },
77737770 });
7771+
7772+ if (fetchResult.ok) {
7773+ const data = await fetchResult.json();
7774+ if (data.res) {
7775+ await displayPastChats();
7776+ }
7777+ }
7778+
7779+ if (eventTarget instanceof HTMLInputElement) {
7780+ eventTarget.value = '';
7781+ }
77747782}
77757783
77767784function updateViewMessageIds(startFromZero = false) {
@@ -8225,102 +8233,6 @@ async function createOrEditCharacter(e) {
82258233 }
82268234}
82278235
8228-window['SillyTavern'].getContext = function () {
8229- return {
8230- chat: chat,
8231- characters: characters,
8232- groups: groups,
8233- name1: name1,
8234- name2: name2,
8235- characterId: this_chid,
8236- groupId: selected_group,
8237- chatId: selected_group
8238- ? groups.find(x => x.id == selected_group)?.chat_id
8239- : (this_chid && characters[this_chid] && characters[this_chid].chat),
8240- getCurrentChatId: getCurrentChatId,
8241- getRequestHeaders: getRequestHeaders,
8242- reloadCurrentChat: reloadCurrentChat,
8243- renameChat: renameChat,
8244- saveSettingsDebounced: saveSettingsDebounced,
8245- onlineStatus: online_status,
8246- maxContext: Number(max_context),
8247- chatMetadata: chat_metadata,
8248- streamingProcessor,
8249- eventSource: eventSource,
8250- eventTypes: event_types,
8251- addOneMessage: addOneMessage,
8252- generate: Generate,
8253- sendStreamingRequest: sendStreamingRequest,
8254- sendGenerationRequest: sendGenerationRequest,
8255- stopGeneration: stopGeneration,
8256- getTokenCount: getTokenCount,
8257- extensionPrompts: extension_prompts,
8258- setExtensionPrompt: setExtensionPrompt,
8259- updateChatMetadata: updateChatMetadata,
8260- saveChat: saveChatConditional,
8261- openCharacterChat: openCharacterChat,
8262- openGroupChat: openGroupChat,
8263- saveMetadata: saveMetadata,
8264- sendSystemMessage: sendSystemMessage,
8265- activateSendButtons,
8266- deactivateSendButtons,
8267- saveReply,
8268- substituteParams,
8269- substituteParamsExtended,
8270- SlashCommandParser,
8271- SlashCommand,
8272- SlashCommandArgument,
8273- SlashCommandNamedArgument,
8274- ARGUMENT_TYPE,
8275- executeSlashCommandsWithOptions,
8276- /** @deprecated Use SlashCommandParser.addCommandObject() instead */
8277- registerSlashCommand: registerSlashCommand,
8278- /** @deprecated Use executeSlashCommandWithOptions instead */
8279- executeSlashCommands: executeSlashCommands,
8280- timestampToMoment: timestampToMoment,
8281- /** @deprecated Handlebars for extensions are no longer supported. */
8282- registerHelper: () => { },
8283- registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
8284- unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
8285- registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
8286- unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
8287- isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
8288- canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
8289- registerDebugFunction: registerDebugFunction,
8290- /** @deprecated Use renderExtensionTemplateAsync instead. */
8291- renderExtensionTemplate: renderExtensionTemplate,
8292- renderExtensionTemplateAsync: renderExtensionTemplateAsync,
8293- registerDataBankScraper: ScraperManager.registerDataBankScraper,
8294- /** @deprecated Use callGenericPopup or Popup instead. */
8295- callPopup: callPopup,
8296- callGenericPopup: callGenericPopup,
8297- showLoader: showLoader,
8298- hideLoader: hideLoader,
8299- mainApi: main_api,
8300- extensionSettings: extension_settings,
8301- ModuleWorkerWrapper: ModuleWorkerWrapper,
8302- getTokenizerModel: getTokenizerModel,
8303- generateQuietPrompt: generateQuietPrompt,
8304- writeExtensionField: writeExtensionField,
8305- getThumbnailUrl: getThumbnailUrl,
8306- selectCharacterById: selectCharacterById,
8307- messageFormatting: messageFormatting,
8308- shouldSendOnEnter: shouldSendOnEnter,
8309- isMobile: isMobile,
8310- t: t,
8311- translate: translate,
8312- tags: tags,
8313- tagMap: tag_map,
8314- menuType: menu_type,
8315- createCharacterData: create_save,
8316- /** @deprecated Legacy snake-case naming, compatibility with old extensions */
8317- event_types: event_types,
8318- Popup: Popup,
8319- POPUP_TYPE: POPUP_TYPE,
8320- POPUP_RESULT: POPUP_RESULT,
8321- };
8322-};
8323-
83248236/**
83258237 * Formats a counter for a swipe view.
83268238 * @param {number} current The current number of items.
@@ -10827,13 +10739,13 @@ jQuery(async function () {
1082710739 });
1082810740
1082910741 $('#chat_import_file').on('change', async function (e) {
1083010742 varconst file = e.target.files[0];
1083110743
1083210744 if (!file) {
1083310745 return;
1083410746 }
1083510747
1083610748 varconst ext = file.name.match(/\.(\w+)$/);
1083710749 if (
1083810750 !ext ||
1083910751 (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl')
@@ -10846,17 +10758,17 @@ jQuery(async function () {
1084610758 return;
1084710759 }
1084810760
1084910761 varconst format = ext[1].toLowerCase();
1085010762 $('#chat_import_file_type').val(format);
1085110763
1085210764 varconst formData = new FormData($('#form_import_chat').get(0));
1085310765 formData.append('user_name', name1);
1085410766 $('#select_chat_div').html('');
1085510767
1085610768 if (selected_group) {
1085710769 await importGroupChat(formData, e.originalEvent.target);
1085810770 } else {
1085910771 await importCharacterChat(formData, e.originalEvent.target);
1086010772 }
1086110773 });
1086210774
public/scripts/char-data.js+1 -0
@@ -75,6 +75,7 @@
7575 * @property {string} [source_url] - The source URL associated with the character.
7676 * @property {{full_path: string}} [chub] - The Chub-specific data associated with the character.
7777 * @property {{source: string[]}} [risuai] - The RisuAI-specific data associated with the character.
78+ * @property {{positive: string, negative: string}} [sd_character_prompt] - SD-specific data associated with the character.
7879 */
7980
8081/**
public/scripts/extensions.js+1 -1
@@ -5,6 +5,7 @@ import { showLoader } from './loader.js';
55import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
66import { renderTemplate, renderTemplateAsync } from './templates.js';
77import { isSubsetOf, setValueByPath } from './utils.js';
8+import { getContext } from './st-context.js';
89export {
910 getContext,
1011 getApiUrl,
@@ -174,7 +175,6 @@ const extension_settings = {
174175let modules = [];
175176let activeExtensions = new Set();
176177
177-const getContext = () => window['SillyTavern'].getContext();
178178const getApiUrl = () => extension_settings.apiUrl;
179179let connectedToApi = false;
180180
public/scripts/extensions/caption/settings.html+1 -0
@@ -62,6 +62,7 @@
6262 <option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
6363 <option data-type="google" value="gemini-exp-1114">gemini-exp-1114</option>
6464 <option data-type="google" value="gemini-exp-1121">gemini-exp-1121</option>
65+ <option data-type="google" value="gemini-exp-1206">gemini-exp-1206</option>
6566 <option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</option>
6667 <option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
6768 <option data-type="google" value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
public/scripts/extensions/stable-diffusion/index.js+0 -15
@@ -60,7 +60,6 @@ import { ToolManager } from '../../tool-calling.js';
6060export { MODULE_NAME };
6161
6262const MODULE_NAME = 'sd';
63-const UPDATE_INTERVAL = 1000;
6463// This is a 1x1 transparent PNG
6564const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
6665const CUSTOM_STOP_EVENT = 'sd_stop_generation';
@@ -3687,8 +3686,6 @@ async function addSDGenButtons() {
36873686 const button = $('#sd_gen');
36883687 const dropdown = $('#sd_dropdown');
36893688 dropdown.hide();
3690- button.hide();
3691- messageButton.hide();
36923689
36933690 let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
36943691 placement: 'top',
@@ -3770,18 +3767,6 @@ function isValidState() {
37703767 }
37713768}
37723769
3773-async function moduleWorker() {
3774- if (isValidState()) {
3775- $('#sd_gen').show();
3776- $('.sd_message_gen').show();
3777- }
3778- else {
3779- $('#sd_gen').hide();
3780- $('.sd_message_gen').hide();
3781- }
3782-}
3783-
3784-setInterval(moduleWorker, UPDATE_INTERVAL);
37853770let buttonAbortController = null;
37863771
37873772async function sdMessageButton(e) {
public/scripts/extensions/stable-diffusion/style.css+1 -1
@@ -4,7 +4,7 @@
44
55#sd_dropdown {
66 z-index: 30000;
77 backdrop-filter: blur(var(--SmartThemeBlurStrength));
88}
99
1010#sd_comfy_open_workflow_editor {
public/scripts/extensions/tts/style.css+1 -1
@@ -93,7 +93,7 @@
9393.at-settings-separator {
9494 margin-top: 10px;
9595 margin-bottom: 10px;
9696 padding: 18x18px;
9797 font-weight: bold;
9898 border-top: 1px solid #e1e1e1; /* Grey line */
9999 border-bottom: 1px solid #e1e1e1; /* Grey line */
public/scripts/group-chats.js+39 -31
@@ -1265,18 +1265,20 @@ function getGroupCharacters({ doFilter, onlyMembers } = {}) {
12651265 const thisGroup = openGroupId && groups.find((x) => x.id == openGroupId);
12661266 let candidates = characters
12671267 .filter((x) => isGroupMember(thisGroup, x.avatar) == onlyMembers)
12681268 .map((x, index) => ({ item: x, id: indexcharacters.indexOf(x), type: 'character' }));
1269+
1270+ if (doFilter) {
1271+ candidates = groupCandidatesFilter.applyFilters(candidates);
1272+ }
12691273
12701274 if (onlyMembers) {
12711275 candidates.sort(sortMembersFn);
12721276 } else {
1273- sortEntitiesList(candidates);
1277+ const useFilterOrder = doFilter && !!$('#rm_group_filter').val();
1274- }
1278+ sortEntitiesList(candidates, useFilterOrder, groupCandidatesFilter);
1275-
1276- if (doFilter) {
1277- candidates = groupCandidatesFilter.applyFilters(candidates);
12781279 }
12791280
1281+ groupCandidatesFilter.clearFuzzySearchCaches();
12801282 return candidates;
12811283}
12821284
@@ -1863,32 +1865,38 @@ export async function deleteGroupChat(groupId, chatId) {
18631865 }
18641866}
18651867
1866-export async function importGroupChat(formData) {
1868+/**
1867- await jQuery.ajax({
1869+ * Imports a group chat from a file and adds it to the group.
1868- type: 'POST',
1870+ * @param {FormData} formData Form data to send to the server
1869- url: '/api/chats/group/import',
1871+ * @param {EventTarget} eventTarget Element that triggered the import
1870- data: formData,
1872+ */
18711873 beforeSend:export async function importGroupChat(formData, eventTarget) {
1872- },
1874+ const headers = getRequestHeaders();
1873- cache: false,
1875+ delete headers['Content-Type'];
1874- contentType: false,
1876+ const fetchResult = await fetch('/api/chats/group/import', {
18751877 processDatamethod: false'POST',
1876- success: async function (data) {
1878+ headers: headers,
1877- if (data.res) {
1879+ body: formData,
1878- const chatId = data.res;
1880+ cache: 'no-cache',
1879- const group = groups.find(x => x.id == selected_group);
1880-
1881- if (group) {
1882- group.chats.push(chatId);
1883- await editGroup(selected_group, true, true);
1884- await displayPastChats();
1885- }
1886- }
1887- },
1888- error: function () {
1889- $('#create_button').removeAttr('disabled');
1890- },
18911881 });
1882+
1883+ if (fetchResult.ok) {
1884+ const data = await fetchResult.json();
1885+ if (data.res) {
1886+ const chatId = data.res;
1887+ const group = groups.find(x => x.id == selected_group);
1888+
1889+ if (group) {
1890+ group.chats.push(chatId);
1891+ await editGroup(selected_group, true, true);
1892+ await displayPastChats();
1893+ }
1894+ }
1895+ }
1896+
1897+ if (eventTarget instanceof HTMLInputElement) {
1898+ eventTarget.value = '';
1899+ }
18921900}
18931901
18941902export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
public/scripts/openai.js+5 -4
@@ -379,8 +379,8 @@ export let selected_proxy = proxies[0];
379379let openai_setting_names;
380380let openai_settings;
381381
382-
382+/** @type {import('./PromptManager.js').PromptManager} */
383383export let promptManager = null;
384384
385385async function validateReverseProxy() {
386386 if (!oai_settings.reverse_proxy) {
@@ -1812,6 +1812,7 @@ async function sendOpenAIRequest(type, messages, signal) {
18121812 const isPerplexity = oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY;
18131813 const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ;
18141814 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
1815+ const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
18151816 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
18161817 const isQuiet = type === 'quiet';
18171818 const isImpersonate = type === 'impersonate';
@@ -1971,7 +1972,7 @@ async function sendOpenAIRequest(type, messages, signal) {
19711972 delete generate_data.stop;
19721973 }
19731974
19741975 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
19751976 generate_data['seed'] = oai_settings.seed;
19761977 }
19771978
@@ -4076,7 +4077,7 @@ async function onModelChange() {
40764077 $('#openai_max_context').attr('max', max_2mil);
40774078 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121')) {
40784079 $('#openai_max_context').attr('max', max_32k);
40794080 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {
40804081 $('#openai_max_context').attr('max', max_2mil);
40814082 } else if (value.includes('gemini-1.5-flash')) {
40824083 $('#openai_max_context').attr('max', max_1mil);
public/scripts/power-user.js+13 -12
@@ -60,7 +60,6 @@ export {
6060 loadMovingUIState,
6161 collapseNewlines,
6262 playMessageSound,
63- sortEntitiesList,
6463 fixMarkdown,
6564 power_user,
6665 send_on_enter_options,
@@ -473,9 +472,9 @@ function switchCompactInputArea() {
473472 $('#compact_input_area').prop('checked', power_user.compact_input_area);
474473}
475474
476475export function switchSwipeNumAllMessages() {
477476 $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
478477 $('.mes:not(.last_mes) .swipes-counterbody').csstoggleClass('opacityswipeAllMessages', '').toggle(!!power_user.show_swipe_num_all_messages);
479478}
480479
481480var originalSliderValues = [];
@@ -1837,7 +1836,7 @@ async function loadContextSettings() {
18371836 * Common function to perform fuzzy search with optional caching
18381837 * @param {string} type - Type of search from fuzzySearchCategories
18391838 * @param {any[]} data - Data array to search in
18401839 * @param {Array<{name: string, weight: number, getFn?: Function(obj: any) => string}>} keys - Fuse.js keys configuration
18411840 * @param {string} searchValue - The search term
18421841 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
18431842 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
@@ -1851,7 +1850,6 @@ function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = n
18511850 }
18521851 }
18531852
1854- // @ts-ignore
18551853 const fuse = new Fuse(data, {
18561854 keys: keys,
18571855 includeScore: true,
@@ -1925,7 +1923,7 @@ export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null)
19251923 const mappedData = data.map(x => ({
19261924 key: x,
19271925 name: power_user.personas[x] ?? '',
19281926 description: power_user.persona_descriptions[x]?.description ?? '',
19291927 }));
19301928
19311929 const keys = [
@@ -2080,19 +2078,22 @@ const compareFunc = (first, second) => {
20802078/**
20812079 * Sorts an array of entities based on the current sort settings
20822080 * @param {any[]} entities An array of objects with an `item` property
2081+ * @param {boolean} forceSearch Whether to force search sorting
2082+ * @param {import('./filters.js').FilterHelper} [filterHelper=null] Filter helper to use
20832083 */
20842084export function sortEntitiesList(entities, forceSearch, filterHelper = null) {
2085+ filterHelper = filterHelper ?? entitiesFilter;
20852086 if (power_user.sort_field == undefined || entities.length === 0) {
20862087 return;
20872088 }
20882089
2089- if (power_user.sort_order === 'random') {
2090+ const isSearch = forceSearch || $('#character_sort_order option[data-field="search"]').is(':selected');
2091+
2092+ if (!isSearch && power_user.sort_order === 'random') {
20902093 shuffle(entities);
20912094 return;
20922095 }
20932096
2094- const isSearch = $('#character_sort_order option[data-field="search"]').is(':selected');
2095-
20962097 entities.sort((a, b) => {
20972098 // Sort tags/folders will always be at the top
20982099 if (a.type === 'tag' && b.type !== 'tag') {
@@ -2104,8 +2105,8 @@ function sortEntitiesList(entities) {
21042105
21052106 // If we have search sorting, we take scores and use those
21062107 if (isSearch) {
21072108 const aScore = entitiesFilterfilterHelper.getScore(FILTER_TYPES.SEARCH, `${a.type}.${a.id}`);
21082109 const bScore = entitiesFilterfilterHelper.getScore(FILTER_TYPES.SEARCH, `${b.type}.${b.id}`);
21092110 return (aScore - bScore);
21102111 }
21112112
public/scripts/slash-commands.js+121 -12
@@ -52,7 +52,7 @@ import { hideChatMessageRange } from './chats.js';
5252import { getContext, saveMetadataDebounced } from './extensions.js';
5353import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
5454import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
5555import { chat_completion_sources, oai_settings, setupChatCompletionPromptManagerpromptManager } from './openai.js';
5656import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
5757import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5858import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
@@ -269,7 +269,7 @@ export function initDefaultSlashCommands() {
269269 }),
270270 SlashCommandNamedArgument.fromProps({
271271 name: 'at',
272272 description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
273273 typeList: [ARGUMENT_TYPE.NUMBER],
274274 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
275275 }),
@@ -325,7 +325,7 @@ export function initDefaultSlashCommands() {
325325 ),
326326 SlashCommandNamedArgument.fromProps({
327327 name: 'at',
328328 description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
329329 typeList: [ARGUMENT_TYPE.NUMBER],
330330 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
331331 }),
@@ -388,7 +388,7 @@ export function initDefaultSlashCommands() {
388388 ),
389389 SlashCommandNamedArgument.fromProps({
390390 name: 'at',
391391 description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
392392 typeList: [ARGUMENT_TYPE.NUMBER],
393393 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
394394 }),
@@ -606,7 +606,7 @@ export function initDefaultSlashCommands() {
606606 ),
607607 SlashCommandNamedArgument.fromProps({
608608 name: 'at',
609609 description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values (including -0) are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
610610 typeList: [ARGUMENT_TYPE.NUMBER],
611611 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
612612 }),
@@ -1699,6 +1699,49 @@ export function initDefaultSlashCommands() {
16991699 helpString: 'Sets the model for the current API. Gets the current model name if no argument is provided.',
17001700 }));
17011701 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1702+ name: 'getpromptentry',
1703+ aliases: ['getpromptentries'],
1704+ callback: getPromptEntryCallback,
1705+ returns: 'true/false state of prompt(s)',
1706+ namedArgumentList: [
1707+ SlashCommandNamedArgument.fromProps({
1708+ name: 'identifier',
1709+ description: 'Prompt entry identifier(s) to retrieve',
1710+ typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
1711+ acceptsMultiple: true,
1712+ enumProvider: () =>
1713+ promptManager.serviceSettings.prompts
1714+ .map(prompt => prompt.identifier)
1715+ .map(identifier => new SlashCommandEnumValue(identifier)),
1716+ }),
1717+ SlashCommandNamedArgument.fromProps({
1718+ name: 'name',
1719+ description: 'Prompt entry name(s) to retrieve',
1720+ typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
1721+ acceptsMultiple: true,
1722+ enumProvider: () =>
1723+ promptManager.serviceSettings.prompts
1724+ .map(prompt => prompt.name)
1725+ .map(name => new SlashCommandEnumValue(name)),
1726+ }),
1727+ SlashCommandNamedArgument.fromProps({
1728+ name: 'return',
1729+ description: 'Whether the return will be simple, a list, or a dict.',
1730+ typeList: [ARGUMENT_TYPE.STRING],
1731+ defaultValue: 'simple',
1732+ enumList: ['simple', 'list', 'dict'],
1733+ }),
1734+ ],
1735+ helpString: `
1736+ <div>
1737+ Gets the state of the specified prompt entries.
1738+ </div>
1739+ <div>
1740+ If <code>return</code> is <code>simple</code> (default) then the return will be a single value if only one value was retrieved; otherwise uses a dict (if the identifier parameter was used) or a list.
1741+ </div>
1742+ `,
1743+ }));
1744+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
17021745 name: 'setpromptentry',
17031746 aliases: ['setpromptentries'],
17041747 callback: setPromptEntryCallback,
@@ -1709,7 +1752,6 @@ export function initDefaultSlashCommands() {
17091752 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
17101753 acceptsMultiple: true,
17111754 enumProvider: () => {
1712- const promptManager = setupChatCompletionPromptManager(oai_settings);
17131755 const prompts = promptManager.serviceSettings.prompts;
17141756 return prompts.map(prompt => new SlashCommandEnumValue(prompt.identifier, prompt.name, enumTypes.enum));
17151757 },
@@ -1720,7 +1762,6 @@ export function initDefaultSlashCommands() {
17201762 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.LIST],
17211763 acceptsMultiple: true,
17221764 enumProvider: () => {
1723- const promptManager = setupChatCompletionPromptManager(oai_settings);
17241765 const prompts = promptManager.serviceSettings.prompts;
17251766 return prompts.map(prompt => new SlashCommandEnumValue(prompt.name, prompt.identifier, enumTypes.enum));
17261767 },
@@ -3025,7 +3066,7 @@ async function sendUserMessageCallback(args, text) {
30253066 let insertAt = Number(args?.at);
30263067
30273068 // Convert possible depth parameter to index
30283069 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt ===, Number(-0))) {
30293070 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
30303071 insertAt = chat.length + insertAt;
30313072 }
@@ -3399,7 +3440,7 @@ export async function sendMessageAs(args, text) {
33993440 let insertAt = Number(args.at);
34003441
34013442 // Convert possible depth parameter to index
34023443 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt ===, Number(-0))) {
34033444 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
34043445 insertAt = chat.length + insertAt;
34053446 }
@@ -3453,7 +3494,7 @@ export async function sendNarratorMessage(args, text) {
34533494 let insertAt = Number(args.at);
34543495
34553496 // Convert possible depth parameter to index
34563497 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt ===, Number(-0))) {
34573498 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
34583499 insertAt = chat.length + insertAt;
34593500 }
@@ -3542,7 +3583,7 @@ async function sendCommentMessage(args, text) {
35423583 let insertAt = Number(args.at);
35433584
35443585 // Convert possible depth parameter to index
35453586 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt ===, Number(-0))) {
35463587 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
35473588 insertAt = chat.length + insertAt;
35483589 }
@@ -3787,6 +3828,75 @@ function modelCallback(args, model) {
37873828}
37883829
37893830/**
3831+ * Gets the state of prompt entries (toggles) either via identifier/uuid or name.
3832+ * @param {object} args Object containing arguments
3833+ * @param {string} args.identifier Select prompt entry using an identifier (uuid)
3834+ * @param {string} args.name Select prompt entry using name
3835+ * @param {string} args.return The type of return value to use (simple, list, dict)
3836+ * @returns {Object} An object containing the states of the requested prompt entries
3837+ */
3838+function getPromptEntryCallback(args) {
3839+ const prompts = promptManager.serviceSettings.prompts;
3840+ let returnType = args.return ?? 'simple';
3841+
3842+ function parseArgs(arg) {
3843+ // Arg is already an array
3844+ if (Array.isArray(arg)) {
3845+ return arg;
3846+ }
3847+ const list = [];
3848+ try {
3849+ // Arg is a JSON-stringified array
3850+ const parsedArg = JSON.parse(arg);
3851+ list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
3852+ } catch {
3853+ // Arg is a string
3854+ list.push(arg);
3855+ }
3856+ return list;
3857+ }
3858+
3859+ let identifiersList = parseArgs(args.identifier);
3860+ let nameList = parseArgs(args.name);
3861+
3862+ // Check if identifiers exists in prompt, else remove from list
3863+ if (identifiersList.length !== 0) {
3864+ identifiersList = identifiersList.filter(identifier => prompts.some(prompt => prompt.identifier === identifier));
3865+ }
3866+
3867+ if (nameList.length !== 0) {
3868+ nameList.forEach(name => {
3869+ let identifiers = prompts
3870+ .filter(entry => entry.name === name)
3871+ .map(entry => entry.identifier);
3872+ identifiersList = identifiersList.concat(identifiers);
3873+ });
3874+ }
3875+
3876+ // Get the state for each prompt entry
3877+ let promptStates = new Map();
3878+ identifiersList.forEach(identifier => {
3879+ const promptOrderEntry = promptManager.getPromptOrderEntry(promptManager.activeCharacter, identifier);
3880+ if (promptOrderEntry) {
3881+ promptStates.set(identifier, promptOrderEntry.enabled);
3882+ }
3883+ });
3884+
3885+ // If return is simple (default) but more than one prompt state was retrieved, then change return type
3886+ if (returnType === 'simple' && promptStates.size > 1) {
3887+ returnType = args.identifier ? 'dict' : 'list';
3888+ }
3889+
3890+ const result = (() => {
3891+ if (returnType === 'list') return [...promptStates.values()];
3892+ if (returnType === 'dict') return Object.fromEntries(promptStates);
3893+ return [...promptStates.values()][0];
3894+ })();
3895+
3896+ return result;
3897+}
3898+
3899+/**
37903900 * Sets state of prompt entries (toggles) either via identifier/uuid or name.
37913901 * @param {object} args Object containing arguments
37923902 * @param {string} args.identifier Select prompt entry using an identifier (uuid)
@@ -3796,7 +3906,6 @@ function modelCallback(args, model) {
37963906 */
37973907function setPromptEntryCallback(args, targetState) {
37983908 // needs promptManager to manipulate prompt entries
3799- const promptManager = setupChatCompletionPromptManager(oai_settings);
38003909 const prompts = promptManager.serviceSettings.prompts;
38013910
38023911 function parseArgs(arg) {
public/scripts/st-context.js+171 -0
@@ -0,0 +1,171 @@
1+import {
2+ activateSendButtons,
3+ addOneMessage,
4+ callPopup,
5+ characters,
6+ chat,
7+ chat_metadata,
8+ create_save,
9+ deactivateSendButtons,
10+ event_types,
11+ eventSource,
12+ extension_prompts,
13+ Generate,
14+ generateQuietPrompt,
15+ getCurrentChatId,
16+ getRequestHeaders,
17+ getThumbnailUrl,
18+ main_api,
19+ max_context,
20+ menu_type,
21+ messageFormatting,
22+ name1,
23+ name2,
24+ online_status,
25+ openCharacterChat,
26+ reloadCurrentChat,
27+ renameChat,
28+ saveChatConditional,
29+ saveMetadata,
30+ saveReply,
31+ saveSettingsDebounced,
32+ selectCharacterById,
33+ sendGenerationRequest,
34+ sendStreamingRequest,
35+ sendSystemMessage,
36+ setExtensionPrompt,
37+ stopGeneration,
38+ streamingProcessor,
39+ substituteParams,
40+ substituteParamsExtended,
41+ this_chid,
42+ updateChatMetadata,
43+} from '../script.js';
44+import {
45+ extension_settings,
46+ ModuleWorkerWrapper,
47+ renderExtensionTemplate,
48+ renderExtensionTemplateAsync,
49+ writeExtensionField,
50+} from './extensions.js';
51+import { groups, openGroupChat, selected_group } from './group-chats.js';
52+import { t, translate } from './i18n.js';
53+import { hideLoader, showLoader } from './loader.js';
54+import { MacrosParser } from './macros.js';
55+import { oai_settings } from './openai.js';
56+import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
57+import { power_user, registerDebugFunction } from './power-user.js';
58+import { isMobile, shouldSendOnEnter } from './RossAscends-mods.js';
59+import { ScraperManager } from './scrapers.js';
60+import { executeSlashCommands, executeSlashCommandsWithOptions, registerSlashCommand } from './slash-commands.js';
61+import { SlashCommand } from './slash-commands/SlashCommand.js';
62+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
63+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
64+import { tag_map, tags } from './tags.js';
65+import { textgenerationwebui_settings } from './textgen-settings.js';
66+import { getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
67+import { ToolManager } from './tool-calling.js';
68+import { timestampToMoment } from './utils.js';
69+
70+export function getContext() {
71+ return {
72+ chat,
73+ characters,
74+ groups,
75+ name1,
76+ name2,
77+ characterId: this_chid,
78+ groupId: selected_group,
79+ chatId: selected_group
80+ ? groups.find(x => x.id == selected_group)?.chat_id
81+ : (characters[this_chid]?.chat),
82+ getCurrentChatId,
83+ getRequestHeaders,
84+ reloadCurrentChat,
85+ renameChat,
86+ saveSettingsDebounced,
87+ onlineStatus: online_status,
88+ maxContext: Number(max_context),
89+ chatMetadata: chat_metadata,
90+ streamingProcessor,
91+ eventSource,
92+ eventTypes: event_types,
93+ addOneMessage,
94+ generate: Generate,
95+ sendStreamingRequest,
96+ sendGenerationRequest,
97+ stopGeneration,
98+ /** @deprecated Use getTokenCountAsync instead */
99+ getTokenCount,
100+ getTokenCountAsync,
101+ extensionPrompts: extension_prompts,
102+ setExtensionPrompt,
103+ updateChatMetadata,
104+ saveChat: saveChatConditional,
105+ openCharacterChat,
106+ openGroupChat,
107+ saveMetadata,
108+ sendSystemMessage,
109+ activateSendButtons,
110+ deactivateSendButtons,
111+ saveReply,
112+ substituteParams,
113+ substituteParamsExtended,
114+ SlashCommandParser,
115+ SlashCommand,
116+ SlashCommandArgument,
117+ SlashCommandNamedArgument,
118+ ARGUMENT_TYPE,
119+ executeSlashCommandsWithOptions,
120+ /** @deprecated Use SlashCommandParser.addCommandObject() instead */
121+ registerSlashCommand,
122+ /** @deprecated Use executeSlashCommandWithOptions instead */
123+ executeSlashCommands,
124+ timestampToMoment,
125+ /** @deprecated Handlebars for extensions are no longer supported. */
126+ registerHelper: () => { },
127+ registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
128+ unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
129+ registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
130+ unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
131+ isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
132+ canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
133+ registerDebugFunction,
134+ /** @deprecated Use renderExtensionTemplateAsync instead. */
135+ renderExtensionTemplate,
136+ renderExtensionTemplateAsync,
137+ registerDataBankScraper: ScraperManager.registerDataBankScraper.bind(ScraperManager),
138+ /** @deprecated Use callGenericPopup or Popup instead. */
139+ callPopup,
140+ callGenericPopup,
141+ showLoader,
142+ hideLoader,
143+ mainApi: main_api,
144+ extensionSettings: extension_settings,
145+ ModuleWorkerWrapper,
146+ getTokenizerModel,
147+ generateQuietPrompt,
148+ writeExtensionField,
149+ getThumbnailUrl,
150+ selectCharacterById,
151+ messageFormatting,
152+ shouldSendOnEnter,
153+ isMobile,
154+ t,
155+ translate,
156+ tags,
157+ tagMap: tag_map,
158+ menuType: menu_type,
159+ createCharacterData: create_save,
160+ /** @deprecated Legacy snake-case naming, compatibility with old extensions */
161+ event_types: event_types,
162+ Popup,
163+ POPUP_TYPE,
164+ POPUP_RESULT,
165+ chatCompletionSettings: oai_settings,
166+ textCompletionSettings: textgenerationwebui_settings,
167+ powerUserSettings: power_user,
168+ };
169+}
170+
171+export default getContext;
public/scripts/templates/itemizationChat.html+11 -2
@@ -4,8 +4,17 @@
44 <div id="copyPromptToClipboard" class="fa-solid fa-copy menu_button" title="Copy Prompt" data-i18n="[title]Copy Prompt"></div>
55 <div id="diffPrevPrompt" class="fa-solid fa-code-compare menu_button" title="Show Prompt Differences" data-i18n="[title]Show Prompt Differences"></div>
66</h3>
7-API/Model Used: {{mainApiFriendlyName}} {{#if apiUsed}}({{apiUsed}}){{/if}} {{#if modelUsed}}&ndash; {{modelUsed}}{{/if}}<br>
7+<div>
8-Tokenizer: {{selectedTokenizer}}<br>
8+ <div>
9+ API/Model: {{mainApiFriendlyName}} {{#if apiUsed}}({{apiUsed}}){{/if}} {{#if modelUsed}}&ndash; {{modelUsed}}{{/if}}
10+ </div>
11+ <div>
12+ <small>Preset: {{presetName}}</small>
13+ <span>|</span>
14+ <small>Tokenizer: {{selectedTokenizer}}</small>
15+ </div>
16+</div>
17+
918<span class="tokenItemizingSubclass">
1019 Only the white numbers really matter. All numbers are estimates.
1120 Grey color items may not have been included in the context due to certain prompt format settings.
public/scripts/templates/itemizationText.html+11 -2
@@ -4,8 +4,17 @@
44 <div id="copyPromptToClipboard" class="fa-solid fa-copy menu_button" title="Copy Prompt" data-i18n="[title]Copy Prompt"></div>
55 <div id="diffPrevPrompt" class="fa-solid fa-code-compare menu_button" title="Show Prompt Differences" data-i18n="[title]Show Prompt Differences"></div>
66</h3>
7-API/Model Used: {{mainApiFriendlyName}} {{#if apiUsed}}({{apiUsed}}){{/if}} {{#if modelUsed}}&ndash; {{modelUsed}}{{/if}}<br>
7+<div>
8-Tokenizer: {{selectedTokenizer}}<br>
8+ <div>
9+ API/Model: {{mainApiFriendlyName}} {{#if apiUsed}}({{apiUsed}}){{/if}} {{#if modelUsed}}&ndash; {{modelUsed}}{{/if}}
10+ </div>
11+ <div>
12+ <small>Preset: {{presetName}}</small>
13+ <span>|</span>
14+ <small>Tokenizer: {{selectedTokenizer}}</small>
15+ </div>
16+</div>
17+
918<span class="tokenItemizingSubclass">
1019 Only the white numbers really matter. All numbers are estimates.
1120 Grey color items may not have been included in the context due to certain prompt format settings.
public/scripts/templates/macros.html+2 -1
@@ -21,7 +21,8 @@
2121 <li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> – <span data-i18n="help_macros_15">your current Persona username</span></li>
2222 <li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> – <span data-i18n="help_macros_16">the Character's name</span></li>
2323 <li><tt>&lcub;&lcub;char_version&rcub;&rcub;</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li>
2424 <li><tt>&lcub;&lcub;group&rcub;&rcub;</tt> – <span data-i18n="help_macros_18">a comma-separated list of group member names (including muted) or the character name in solo chats. Alias: &lcub;&lcub;charIfNotGroup&rcub;&rcub;</span></li>
25+ <li><tt>&lcub;&lcub;groupNotMuted&rcub;&rcub;</tt> – <span data-i18n="help_groupNotMuted">the same as &lcub;&lcub;group&rcub;&rcub;, but excludes muted members</span></li>
2526 <li><tt>&lcub;&lcub;model&rcub;&rcub;</tt> – <span data-i18n="help_macros_19">a text generation model name for the currently selected API. </span><b data-i18n="Can be inaccurate!">Can be inaccurate!</b></li>
2627 <li><tt>&lcub;&lcub;lastMessage&rcub;&rcub;</tt> – <span data-i18n="help_macros_20">the text of the latest chat message.</span></li>
2728 <li><tt>&lcub;&lcub;lastUserMessage&rcub;&rcub;</tt> – <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li>
public/scripts/textgen-settings.js+11 -10
@@ -18,13 +18,6 @@ import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer }
1818import { ENCODE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
1919import { getSortableDelay, onlyUnique } from './utils.js';
2020
21-export {
22- settings as textgenerationwebui_settings,
23- loadTextGenSettings,
24- generateTextGenWithStreaming,
25- formatTextGenURL,
26-};
27-
2821export const textgen_types = {
2922 OOBA: 'ooba',
3023 MANCER: 'mancer',
@@ -214,6 +207,10 @@ const settings = {
214207 featherless_model: '',
215208};
216209
210+export {
211+ settings as textgenerationwebui_settings,
212+};
213+
217214export let textgenerationwebui_banned_in_macros = [];
218215
219216export let textgenerationwebui_presets = [];
@@ -345,7 +342,7 @@ async function selectPreset(name) {
345342 saveSettingsDebounced();
346343}
347344
348345export function formatTextGenURL(value) {
349346 try {
350347 const noFormatTypes = [MANCER, TOGETHERAI, INFERMATICAI, DREAMGEN, OPENROUTER];
351348 if (noFormatTypes.includes(settings.type)) {
@@ -483,7 +480,7 @@ function calculateLogitBias() {
483480 return result;
484481}
485482
486483export function loadTextGenSettings(data, loadedSettings) {
487484 textgenerationwebui_presets = convertPresets(data.textgenerationwebui_presets);
488485 textgenerationwebui_preset_names = data.textgenerationwebui_preset_names ?? [];
489486 Object.assign(settings, loadedSettings.textgenerationwebui_settings ?? {});
@@ -922,7 +919,7 @@ function setSettingByName(setting, value, trigger) {
922919 * @returns {Promise<(function(): AsyncGenerator<{swipes: [], text: string, toolCalls: [], logprobs: {token: string, topLogprobs: Candidate[]}|null}, void, *>)|*>}
923920 * @throws {Error} - If the response status is not OK, or from within the generator
924921 */
925922export async function generateTextGenWithStreaming(generate_data, signal) {
926923 generate_data.stream = true;
927924
928925 const response = await fetch('/api/backends/text-completions/generate', {
@@ -1302,6 +1299,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
13021299
13031300 if (settings.type === KOBOLDCPP) {
13041301 params.grammar = settings.grammar_string;
1302+ params.trim_stop = true;
13051303 }
13061304
13071305 if (settings.type === HUGGINGFACE) {
@@ -1373,6 +1371,9 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
13731371 'dry_sequence_breakers': sequenceBreakers,
13741372 };
13751373 params = Object.assign(params, llamaCppParams);
1374+ if (!Array.isArray(sequenceBreakers) || sequenceBreakers.length === 0) {
1375+ delete params.dry_sequence_breakers;
1376+ }
13761377 }
13771378
13781379 eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
public/scripts/tokenizers.js+20 -10
@@ -8,8 +8,6 @@ import { kai_flags } from './kai-settings.js';
88import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer, getTextGenModel } from './textgen-settings.js';
99import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer, openRouterModels } from './textgen-models.js';
1010
11-const { OOBA, TABBY, KOBOLDCPP, VLLM, APHRODITE, LLAMACPP, OPENROUTER, DREAMGEN } = textgen_types;
12-
1311export const CHARACTERS_PER_TOKEN_RATIO = 3.35;
1412export const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';
1513export const TOKENIZER_SUPPORTED_KEY = 'tokenizationSupported';
@@ -52,8 +50,12 @@ export const ENCODE_TOKENIZERS = [
5250 //tokenizers.NERD2,
5351];
5452
55-// A list of Text Completion sources that support remote tokenization.
53+/**
56-export const TEXTGEN_TOKENIZERS = [OOBA, TABBY, KOBOLDCPP, LLAMACPP, VLLM, APHRODITE];
54+ * A list of Text Completion sources that support remote tokenization.
55+ * Populated in initTokenziers due to circular dependencies.
56+ * @type {string[]}
57+ */
58+export const TEXTGEN_TOKENIZERS = [];
5759
5860const TOKENIZER_URLS = {
5961 [tokenizers.GPT2]: {
@@ -287,7 +289,7 @@ export function getTokenizerBestMatch(forApi) {
287289 const hasTokenizerError = sessionStorage.getItem(TOKENIZER_WARNING_KEY);
288290 const hasValidEndpoint = sessionStorage.getItem(TOKENIZER_SUPPORTED_KEY);
289291 const isConnected = online_status !== 'no_connection';
290292 const isTokenizerSupported = TEXTGEN_TOKENIZERS.includes(textgen_settings.type) && (textgen_settings.type !== textgen_types.OOBA || hasValidEndpoint);
291293
292294 if (!hasTokenizerError && isConnected) {
293295 if (forApi === 'kobold' && kai_flags.can_use_tokenization) {
@@ -297,10 +299,10 @@ export function getTokenizerBestMatch(forApi) {
297299 if (forApi === 'textgenerationwebui' && isTokenizerSupported) {
298300 return tokenizers.API_TEXTGENERATIONWEBUI;
299301 }
300302 if (forApi === 'textgenerationwebui' && textgen_settings.type === textgen_types.OPENROUTER) {
301303 return getCurrentOpenRouterModelTokenizer();
302304 }
303305 if (forApi === 'textgenerationwebui' && textgen_settings.type === textgen_types.DREAMGEN) {
304306 return getCurrentDreamGenModelTokenizer();
305307 }
306308 }
@@ -576,7 +578,7 @@ export function getTokenizerModel() {
576578
577579 // And for OpenRouter (if not a site model, then it's impossible to determine the tokenizer)
578580 if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model ||
579581 main_api == 'textgenerationwebui' && textgen_settings.type === textgen_types.OPENROUTER && textgen_settings.openrouter_model) {
580582 const model = main_api == 'openai'
581583 ? model_list.find(x => x.id === oai_settings.openrouter_model)
582584 : openRouterModels.find(x => x.id === textgen_settings.openrouter_model);
@@ -652,7 +654,7 @@ export function getTokenizerModel() {
652654 return oai_settings.custom_model;
653655 }
654656
655657 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
656658 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
657659 return llama3Tokenizer;
658660 }
@@ -680,7 +682,7 @@ export function getTokenizerModel() {
680682 return yiTokenizer;
681683 }
682684
683685 if (oai_settings.chat_completion_source === chat_completion_sources.BLOCKENTROPY) {
684686 if (oai_settings.blockentropy_model.includes('llama3')) {
685687 return llama3Tokenizer;
686688 }
@@ -1121,6 +1123,14 @@ export function decodeTextTokens(tokenizerType, ids) {
11211123}
11221124
11231125export async function initTokenizers() {
1126+ TEXTGEN_TOKENIZERS.push(
1127+ textgen_types.OOBA,
1128+ textgen_types.TABBY,
1129+ textgen_types.KOBOLDCPP,
1130+ textgen_types.LLAMACPP,
1131+ textgen_types.VLLM,
1132+ textgen_types.APHRODITE,
1133+ );
11241134 await loadTokenCache();
11251135 registerDebugFunction('resetTokenCache', 'Reset token cache', 'Purges the calculated token counts. Use this if you want to force a full re-tokenization of all chats or suspect the token counts are wrong.', resetTokenCache);
11261136}
public/scripts/tool-calling.js+1 -1
@@ -947,7 +947,7 @@ export class ToolManager {
947947 enumProvider: toolsEnumProvider,
948948 }),
949949 ],
950950 callback: async (_, name) => {
951951 if (typeof name !== 'string' || !name) {
952952 throw new Error('The unnamed argument must be a non-empty string.');
953953 }
public/style.css+8 -2
@@ -1040,8 +1040,14 @@ body .panelControlBar {
10401040 height: var(--swipeCounterHeight);
10411041}
10421042
10431043body:not(.swipeAllMessages) .mes:not(.last_mes) .swipes-counter {
10441044 opacityvisibility: 0.3hidden;
1045+}
1046+
1047+body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1048+ /* Avoid expensive DOM queries */
1049+ opacity: 0.3 !important;
1050+ display: flex !important;
10451051}
10461052
10471053.swipe_left {
src/endpoints/backends/chat-completions.js+1 -1
@@ -280,7 +280,7 @@ async function sendMakerSuiteRequest(request, response) {
280280 delete generationConfig.stopSequences;
281281 }
282282
283283 const should_use_system_prompt = (model.includes('gemini-1.5-flash') || model.includes('gemini-1.5-pro') || model.includes('gemini-exp-1114') || model.includesstartsWith('gemini-exp-1121')) && request.body.use_makersuite_sysprompt;
284284 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, request.body.char_name, request.body.user_name);
285285 let body = {
286286 contents: prompt.contents,
src/endpoints/characters.js+4 -3
@@ -14,7 +14,7 @@ import jimp from 'jimp';
1414
1515import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1616import { jsonParser, urlencodedParser } from '../express-common.js';
1717import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';
1818import { TavernCardValidator } from '../validator/TavernCardValidator.js';
1919import { parse, write } from '../character-card-parser.js';
2020import { readWorldInfoFile } from './worldinfo.js';
@@ -23,8 +23,9 @@ import { importRisuSprites } from './sprites.js';
2323const defaultAvatarPath = './public/img/ai4.png';
2424
2525// KV-store for parsed character data
26-// 100 MB limit. Would take roughly 3000 characters to reach this limit
26+const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100)); // MB
27-const characterDataCache = new MemoryLimitedMap(1024 * 1024 * 100);
27+// With 100 MB limit it would take roughly 3000 characters to reach this limit
28+const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity);
2829// Some Android devices require tighter memory management
2930const isAndroid = process.platform === 'android';
3031
src/endpoints/chats.js+74 -27
@@ -191,6 +191,44 @@ function importCAIChat(userName, characterName, jsonData) {
191191}
192192
193193/**
194+ * Imports a chat from Kobold Lite format.
195+ * @param {string} _userName User name
196+ * @param {string} _characterName Character name
197+ * @param {object} data JSON data
198+ * @returns {string} Chat data
199+ */
200+function importKoboldLiteChat(_userName, _characterName, data) {
201+ const inputToken = '{{[INPUT]}}';
202+ const outputToken = '{{[OUTPUT]}}';
203+
204+ /** @type {function(string): object} */
205+ function processKoboldMessage(msg) {
206+ const isUser = msg.includes(inputToken) || msg.includes(outputToken);
207+ return {
208+ name: isUser ? header.user_name : header.character_name,
209+ is_user: isUser,
210+ mes: msg.replace(inputToken, '').replace(outputToken, '').trim(),
211+ send_date: Date.now(),
212+ };
213+ }
214+
215+ // Create the header
216+ const header = {
217+ user_name: data.savedsettings.chatname,
218+ character_name: data.savedsettings.chatopponent,
219+ };
220+ // Format messages
221+ const formattedMessages = data.actions.map(processKoboldMessage);
222+ // Add prompt if available
223+ if (data.prompt) {
224+ formattedMessages.unshift(processKoboldMessage(data.prompt));
225+ }
226+ // Combine header and messages
227+ const chatData = [header, ...formattedMessages];
228+ return chatData.map(obj => JSON.stringify(obj)).join('\n');
229+}
230+
231+/**
194232 * Flattens `msg` and `swipes` data from Chub Chat format.
195233 * Only changes enough to make it compatible with the standard chat serialization format.
196234 * @param {string} userName User name
@@ -413,7 +451,7 @@ router.post('/import', urlencodedParser, function (request, response) {
413451 const format = request.body.file_type;
414452 const avatarUrl = (request.body.avatar_url).replace('.png', '');
415453 const characterName = request.body.character_name;
416454 const userName = request.body.user_name || 'YouUser';
417455
418456 if (!request.file) {
419457 return response.sendStatus(400);
@@ -426,33 +464,38 @@ router.post('/import', urlencodedParser, function (request, response) {
426464 if (format === 'json') {
427465 fs.unlinkSync(pathToUpload);
428466 const jsonData = JSON.parse(data);
429- if (jsonData.histories !== undefined) {
467+
430- // CAI Tools format
468+ /** @type {function(string, string, object): string|string[]} */
431- const chats = importCAIChat(userName, characterName, jsonData);
469+ let importFunc;
432- for (const chat of chats) {
470+
433- const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
471+ if (jsonData.savedsettings !== undefined) { // Kobold Lite format
434- const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
472+ importFunc = importKoboldLiteChat;
435- writeFileAtomicSync(filePath, chat, 'utf8');
473+ } else if (jsonData.histories !== undefined) { // CAI Tools format
436- }
474+ importFunc = importCAIChat;
437- return response.send({ res: true });
475+ } else if (Array.isArray(jsonData.data_visible)) { // oobabooga's format
438- } else if (Array.isArray(jsonData.data_visible)) {
476+ importFunc = importOobaChat;
439- // oobabooga's format
477+ } else if (Array.isArray(jsonData.messages)) { // Agnai's format
440- const chat = importOobaChat(userName, characterName, jsonData);
478+ importFunc = importAgnaiChat;
441- const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
479+ } else { // Unknown format
442- const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
480+ console.log('Incorrect chat format .json');
443- writeFileAtomicSync(filePath, chat, 'utf8');
481+ return response.send({ error: true });
444- return response.send({ res: true });
482+ }
445- } else if (Array.isArray(jsonData.messages)) {
483+
446- // Agnai format
484+ const handleChat = (chat) => {
447- const chat = importAgnaiChat(userName, characterName, jsonData);
448485 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
449486 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
450487 writeFileAtomicSync(filePath, chat, 'utf8');
451- return response.send({ res: true });
488+ };
489+
490+ const chat = importFunc(userName, characterName, jsonData);
491+
492+ if (Array.isArray(chat)) {
493+ chat.forEach(handleChat);
452494 } else {
453- console.log('Incorrect chat format .json');
495+ handleChat(chat);
454- return response.send({ error: true });
455496 }
497+
498+ return response.send({ res: true });
456499 }
457500
458501 if (format === 'jsonl') {
@@ -561,10 +604,14 @@ router.post('/search', jsonParser, function (request, response) {
561604
562605 let targetGroup;
563606 for (const groupFile of groupFiles) {
564- const groupData = JSON.parse(fs.readFileSync(path.join(groupDir, groupFile), 'utf8'));
607+ try {
565- if (groupData.id === group_id) {
608+ const groupData = JSON.parse(fs.readFileSync(path.join(groupDir, groupFile), 'utf8'));
566- targetGroup = groupData;
609+ if (groupData.id === group_id) {
567- break;
610+ targetGroup = groupData;
611+ break;
612+ }
613+ } catch (error) {
614+ console.error(groupFile, 'group file is corrupted:', error);
568615 }
569616 }
570617
src/endpoints/users-public.js+8 -8
@@ -67,17 +67,17 @@ router.post('/login', jsonParser, async (request, response) => {
6767 const user = await storage.getItem(toKey(request.body.handle));
6868
6969 if (!user) {
7070 console.log('Login failed: User', request.body.handle, 'not found');
7171 return response.status(403).json({ error: 'Incorrect credentials' });
7272 }
7373
7474 if (!user.enabled) {
7575 console.log('Login failed: User', user.handle, 'is disabled');
7676 return response.status(403).json({ error: 'User is disabled' });
7777 }
7878
7979 if (user.password && user.password !== getPasswordHash(request.body.password, user.salt)) {
8080 console.log('Login failed: Incorrect password for', user.handle);
8181 return response.status(403).json({ error: 'Incorrect credentials' });
8282 }
8383
@@ -88,7 +88,7 @@ router.post('/login', jsonParser, async (request, response) => {
8888
8989 await loginLimiter.delete(ip);
9090 request.session.handle = user.handle;
9191 console.log('Login successful:', user.handle, request'from', ip, 'at', new Date().sessiontoLocaleString());
9292 return response.json({ handle: user.handle });
9393 } catch (error) {
9494 if (error instanceof RateLimiterRes) {
@@ -115,12 +115,12 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
115115 const user = await storage.getItem(toKey(request.body.handle));
116116
117117 if (!user) {
118118 console.log('Recover step 1 failed: User', request.body.handle, 'not found');
119119 return response.status(404).json({ error: 'User not found' });
120120 }
121121
122122 if (!user.enabled) {
123123 console.log('Recover step 1 failed: User', user.handle, 'is disabled');
124124 return response.status(403).json({ error: 'User is disabled' });
125125 }
126126
@@ -153,12 +153,12 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
153153 const ip = getIpFromRequest(request);
154154
155155 if (!user) {
156156 console.log('Recover step 2 failed: User', request.body.handle, 'not found');
157157 return response.status(404).json({ error: 'User not found' });
158158 }
159159
160160 if (!user.enabled) {
161161 console.log('Recover step 2 failed: User', user.handle, 'is disabled');
162162 return response.status(403).json({ error: 'User is disabled' });
163163 }
164164
src/prompt-converters.js+1 -0
@@ -347,6 +347,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN
347347 'gemini-1.5-flash-8b-exp-0924',
348348 'gemini-exp-1114',
349349 'gemini-exp-1121',
350+ 'gemini-exp-1206',
350351 'gemini-1.5-pro',
351352 'gemini-1.5-pro-latest',
352353 'gemini-1.5-pro-001',
src/util.js+3 -2
@@ -680,8 +680,9 @@ export class MemoryLimitedMap {
680680 * @param {number} maxMemoryInBytes - The maximum allowed memory in bytes for string values.
681681 */
682682 constructor(maxMemoryInBytes) {
683683 if (typeof maxMemoryInBytes !== 'number' || maxMemoryInBytes <= 0 || isNaN(maxMemoryInBytes)) {
684- throw new Error('maxMemoryInBytes must be a positive number');
684+ console.warn('Invalid maxMemoryInBytes, using a fallback value of 1 GB.');
685+ maxMemoryInBytes = 1024 * 1024 * 1024; // 1 GB
685686 }
686687 this.maxMemory = maxMemoryInBytes;
687688 this.currentMemory = 0;