Merge branch 'staging' into persona-lorebook

efe25ddc04a0340cb60473189d8d84e5dae7389e

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

10 files changed, +200 -79Showing whitespace changes
.github/readme-ru_ru.md+38 -0
@@ -209,6 +209,44 @@ SillyTavern поддерживает расширения.
2095. Запустите лаунчер установки: `chmod +x install.sh && ./install.sh` and choose what you wanna install2095. Запустите лаунчер установки: `chmod +x install.sh && ./install.sh` and choose what you wanna install
2106. После завершения установки, запустите лаунчер следующей командой: `chmod +x launcher.sh && ./launcher.sh`2106. После завершения установки, запустите лаунчер следующей командой: `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
2431. Откройте командную строку
2442. Выполните следующую команду
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
212## 📱 Мобильные устройства - Установка при помощи termux250## 📱 Мобильные устройства - Установка при помощи termux
213251
214> **ОБРАТИТЕ ВНИМАНИЕ!**252> **ОБРАТИТЕ ВНИМАНИЕ!**
public/index.html+5 -5
@@ -669,7 +669,7 @@
669 </span>669 </span>
670 </div>670 </div>
671 </div>671 </div>
672 <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai">672 <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt">
673 <div class="range-block-title" data-i18n="Temperature">673 <div class="range-block-title" data-i18n="Temperature">
674 Temperature674 Temperature
675 </div>675 </div>
@@ -682,7 +682,7 @@
682 </div>682 </div>
683 </div>683 </div>
684 </div>684 </div>
685 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai">685 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt">
686 <div class="range-block-title" data-i18n="Frequency Penalty">686 <div class="range-block-title" data-i18n="Frequency Penalty">
687 Frequency Penalty687 Frequency Penalty
688 </div>688 </div>
@@ -695,7 +695,7 @@
695 </div>695 </div>
696 </div>696 </div>
697 </div>697 </div>
698 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai">698 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt">
699 <div class="range-block-title" data-i18n="Presence Penalty">699 <div class="range-block-title" data-i18n="Presence Penalty">
700 Presence Penalty700 Presence Penalty
701 </div>701 </div>
@@ -721,7 +721,7 @@
721 </div>721 </div>
722 </div>722 </div>
723 </div>723 </div>
724 <div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai">724 <div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt">
725 <div class="range-block-title" data-i18n="Top P">725 <div class="range-block-title" data-i18n="Top P">
726 Top P726 Top P
727 </div>727 </div>
@@ -958,7 +958,7 @@
958 </div>958 </div>
959 </div>959 </div>
960 </div>960 </div>
961 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq">961 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt">
962 <div class="range-block-title justifyLeft" data-i18n="Seed">962 <div class="range-block-title justifyLeft" data-i18n="Seed">
963 Seed963 Seed
964 </div>964 </div>
public/script.js+31 -23
@@ -241,7 +241,7 @@ import { hideLoader, showLoader } from './scripts/loader.js';
241import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';241import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
242import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels } from './scripts/textgen-models.js';242import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels } from './scripts/textgen-models.js';
243import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js';243import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js';
244import { initPresetManager } from './scripts/preset-manager.js';244import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
245import { MacrosParser, evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';245import { MacrosParser, evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
246import { currentUser, setUserControls } from './scripts/user.js';246import { currentUser, setUserControls } from './scripts/user.js';
247import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';247import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
@@ -4494,6 +4494,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4494 instruction: main_api !== 'openai' && power_user.sysprompt.enabled ? substituteParams(power_user.prefer_character_prompt && system ? system : power_user.sysprompt.content) : '',4494 instruction: main_api !== 'openai' && power_user.sysprompt.enabled ? substituteParams(power_user.prefer_character_prompt && system ? system : power_user.sysprompt.content) : '',
4495 userPersona: (power_user.persona_description_position == persona_description_positions.IN_PROMPT ? (persona || '') : ''),4495 userPersona: (power_user.persona_description_position == persona_description_positions.IN_PROMPT ? (persona || '') : ''),
4496 tokenizer: getFriendlyTokenizerName(main_api).tokenizerName || '',4496 tokenizer: getFriendlyTokenizerName(main_api).tokenizerName || '',
4497 presetName: getPresetManager()?.getSelectedPresetName() || '',
4497 };4498 };
44984499
4499 //console.log(additionalPromptStuff);4500 //console.log(additionalPromptStuff);
@@ -5160,6 +5161,7 @@ export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMes
5160 dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),5161 dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),
5161 modelUsed: chat[incomingMesId]?.extra?.model,5162 modelUsed: chat[incomingMesId]?.extra?.model,
5162 apiUsed: chat[incomingMesId]?.extra?.api,5163 apiUsed: chat[incomingMesId]?.extra?.api,
5164 presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
5163 };5165 };
51645166
5165 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;5167 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
@@ -7752,25 +7754,31 @@ export async function saveChatConditional() {
7752 }7754 }
7753}7755}
77547756
7755async 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 */
7760 beforeSend: function () {7762async 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', {
7764 processData: false,7766 method: 'POST',
7765 success: async function (data) {7767 body: formData,
7768 headers: headers,
7769 cache: 'no-cache',
7770 });
7771
7772 if (fetchResult.ok) {
7773 const data = await fetchResult.json();
7766 if (data.res) {7774 if (data.res) {
7767 await displayPastChats();7775 await displayPastChats();
7768 }7776 }
7769 },7777 }
7770 error: function () {7778
7771 $('#create_button').removeAttr('disabled');7779 if (eventTarget instanceof HTMLInputElement) {
7772 },7780 eventTarget.value = '';
7773 });7781 }
7774}7782}
77757783
7776function updateViewMessageIds(startFromZero = false) {7784function updateViewMessageIds(startFromZero = false) {
@@ -10827,13 +10835,13 @@ jQuery(async function () {
10827 });10835 });
1082810836
10829 $('#chat_import_file').on('change', async function (e) {10837 $('#chat_import_file').on('change', async function (e) {
10830 var file = e.target.files[0];10838 const file = e.target.files[0];
1083110839
10832 if (!file) {10840 if (!file) {
10833 return;10841 return;
10834 }10842 }
1083510843
10836 var ext = file.name.match(/\.(\w+)$/);10844 const ext = file.name.match(/\.(\w+)$/);
10837 if (10845 if (
10838 !ext ||10846 !ext ||
10839 (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl')10847 (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl')
@@ -10846,17 +10854,17 @@ jQuery(async function () {
10846 return;10854 return;
10847 }10855 }
1084810856
10849 var format = ext[1].toLowerCase();10857 const format = ext[1].toLowerCase();
10850 $('#chat_import_file_type').val(format);10858 $('#chat_import_file_type').val(format);
1085110859
10852 var formData = new FormData($('#form_import_chat').get(0));10860 const formData = new FormData($('#form_import_chat').get(0));
10853 formData.append('user_name', name1);10861 formData.append('user_name', name1);
10854 $('#select_chat_div').html('');10862 $('#select_chat_div').html('');
1085510863
10856 if (selected_group) {10864 if (selected_group) {
10857 await importGroupChat(formData);10865 await importGroupChat(formData, e.originalEvent.target);
10858 } else {10866 } else {
10859 await importCharacterChat(formData);10867 await importCharacterChat(formData, e.originalEvent.target);
10860 }10868 }
10861 });10869 });
1086210870
public/scripts/group-chats.js+22 -16
@@ -1863,17 +1863,23 @@ export async function deleteGroupChat(groupId, chatId) {
1863 }1863 }
1864}1864}
18651865
1866export async function importGroupChat(formData) {1866/**
1867 await jQuery.ajax({1867 * Imports a group chat from a file and adds it to the group.
1868 type: 'POST',1868 * @param {FormData} formData Form data to send to the server
1869 url: '/api/chats/group/import',1869 * @param {EventTarget} eventTarget Element that triggered the import
1870 data: formData,1870 */
1871 beforeSend: function () {1871export async function importGroupChat(formData, eventTarget) {
1872 },1872 const headers = getRequestHeaders();
1873 cache: false,1873 delete headers['Content-Type'];
1874 contentType: false,1874 const fetchResult = await fetch('/api/chats/group/import', {
1875 processData: false,1875 method: 'POST',
1876 success: async function (data) {1876 headers: headers,
1877 body: formData,
1878 cache: 'no-cache',
1879 });
1880
1881 if (fetchResult.ok) {
1882 const data = await fetchResult.json();
1877 if (data.res) {1883 if (data.res) {
1878 const chatId = data.res;1884 const chatId = data.res;
1879 const group = groups.find(x => x.id == selected_group);1885 const group = groups.find(x => x.id == selected_group);
@@ -1884,11 +1890,11 @@ export async function importGroupChat(formData) {
1884 await displayPastChats();1890 await displayPastChats();
1885 }1891 }
1886 }1892 }
1887 },1893 }
1888 error: function () {1894
1889 $('#create_button').removeAttr('disabled');1895 if (eventTarget instanceof HTMLInputElement) {
1890 },1896 eventTarget.value = '';
1891 });1897 }
1892}1898}
18931899
1894export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {1900export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
public/scripts/openai.js+2 -1
@@ -1812,6 +1812,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1812 const isPerplexity = oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY;1812 const isPerplexity = oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY;
1813 const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ;1813 const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ;
1814 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;1814 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
1815 const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
1815 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);1816 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
1816 const isQuiet = type === 'quiet';1817 const isQuiet = type === 'quiet';
1817 const isImpersonate = type === 'impersonate';1818 const isImpersonate = type === 'impersonate';
@@ -1971,7 +1972,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1971 delete generate_data.stop;1972 delete generate_data.stop;
1972 }1973 }
19731974
1974 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere) && oai_settings.seed >= 0) {1975 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
1975 generate_data['seed'] = oai_settings.seed;1976 generate_data['seed'] = oai_settings.seed;
1976 }1977 }
19771978
public/scripts/slash-commands.js+8 -8
@@ -269,7 +269,7 @@ export function initDefaultSlashCommands() {
269 }),269 }),
270 SlashCommandNamedArgument.fromProps({270 SlashCommandNamedArgument.fromProps({
271 name: 'at',271 name: 'at',
272 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 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.',272 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.',
273 typeList: [ARGUMENT_TYPE.NUMBER],273 typeList: [ARGUMENT_TYPE.NUMBER],
274 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),274 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
275 }),275 }),
@@ -325,7 +325,7 @@ export function initDefaultSlashCommands() {
325 ),325 ),
326 SlashCommandNamedArgument.fromProps({326 SlashCommandNamedArgument.fromProps({
327 name: 'at',327 name: 'at',
328 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 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.',328 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.',
329 typeList: [ARGUMENT_TYPE.NUMBER],329 typeList: [ARGUMENT_TYPE.NUMBER],
330 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),330 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
331 }),331 }),
@@ -388,7 +388,7 @@ export function initDefaultSlashCommands() {
388 ),388 ),
389 SlashCommandNamedArgument.fromProps({389 SlashCommandNamedArgument.fromProps({
390 name: 'at',390 name: 'at',
391 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 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.',391 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.',
392 typeList: [ARGUMENT_TYPE.NUMBER],392 typeList: [ARGUMENT_TYPE.NUMBER],
393 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),393 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
394 }),394 }),
@@ -606,7 +606,7 @@ export function initDefaultSlashCommands() {
606 ),606 ),
607 SlashCommandNamedArgument.fromProps({607 SlashCommandNamedArgument.fromProps({
608 name: 'at',608 name: 'at',
609 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 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.',609 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.',
610 typeList: [ARGUMENT_TYPE.NUMBER],610 typeList: [ARGUMENT_TYPE.NUMBER],
611 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),611 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
612 }),612 }),
@@ -3025,7 +3025,7 @@ async function sendUserMessageCallback(args, text) {
3025 let insertAt = Number(args?.at);3025 let insertAt = Number(args?.at);
30263026
3027 // Convert possible depth parameter to index3027 // Convert possible depth parameter to index
3028 if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {3028 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
3029 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)3029 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
3030 insertAt = chat.length + insertAt;3030 insertAt = chat.length + insertAt;
3031 }3031 }
@@ -3399,7 +3399,7 @@ export async function sendMessageAs(args, text) {
3399 let insertAt = Number(args.at);3399 let insertAt = Number(args.at);
34003400
3401 // Convert possible depth parameter to index3401 // Convert possible depth parameter to index
3402 if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {3402 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
3403 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)3403 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
3404 insertAt = chat.length + insertAt;3404 insertAt = chat.length + insertAt;
3405 }3405 }
@@ -3453,7 +3453,7 @@ export async function sendNarratorMessage(args, text) {
3453 let insertAt = Number(args.at);3453 let insertAt = Number(args.at);
34543454
3455 // Convert possible depth parameter to index3455 // Convert possible depth parameter to index
3456 if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {3456 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
3457 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)3457 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
3458 insertAt = chat.length + insertAt;3458 insertAt = chat.length + insertAt;
3459 }3459 }
@@ -3542,7 +3542,7 @@ async function sendCommentMessage(args, text) {
3542 let insertAt = Number(args.at);3542 let insertAt = Number(args.at);
35433543
3544 // Convert possible depth parameter to index3544 // Convert possible depth parameter to index
3545 if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {3545 if (!isNaN(insertAt) && (insertAt < 0 || Object.is(insertAt, -0))) {
3546 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)3546 // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
3547 insertAt = chat.length + insertAt;3547 insertAt = chat.length + insertAt;
3548 }3548 }
public/scripts/templates/itemizationChat.html+11 -2
@@ -4,8 +4,17 @@
4 <div id="copyPromptToClipboard" class="fa-solid fa-copy menu_button" title="Copy Prompt" data-i18n="[title]Copy Prompt"></div>4 <div id="copyPromptToClipboard" class="fa-solid fa-copy menu_button" title="Copy Prompt" data-i18n="[title]Copy Prompt"></div>
5 <div id="diffPrevPrompt" class="fa-solid fa-code-compare menu_button" title="Show Prompt Differences" data-i18n="[title]Show Prompt Differences"></div>5 <div id="diffPrevPrompt" class="fa-solid fa-code-compare menu_button" title="Show Prompt Differences" data-i18n="[title]Show Prompt Differences"></div>
6</h3>6</h3>
7API/Model Used: {{mainApiFriendlyName}} {{#if apiUsed}}({{apiUsed}}){{/if}} {{#if modelUsed}}&ndash; {{modelUsed}}{{/if}}<br>7<div>
8Tokenizer: {{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
9<span class="tokenItemizingSubclass">18<span class="tokenItemizingSubclass">
10 Only the white numbers really matter. All numbers are estimates.19 Only the white numbers really matter. All numbers are estimates.
11 Grey color items may not have been included in the context due to certain prompt format settings.20 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 @@
4 <div id="copyPromptToClipboard" class="fa-solid fa-copy menu_button" title="Copy Prompt" data-i18n="[title]Copy Prompt"></div>4 <div id="copyPromptToClipboard" class="fa-solid fa-copy menu_button" title="Copy Prompt" data-i18n="[title]Copy Prompt"></div>
5 <div id="diffPrevPrompt" class="fa-solid fa-code-compare menu_button" title="Show Prompt Differences" data-i18n="[title]Show Prompt Differences"></div>5 <div id="diffPrevPrompt" class="fa-solid fa-code-compare menu_button" title="Show Prompt Differences" data-i18n="[title]Show Prompt Differences"></div>
6</h3>6</h3>
7API/Model Used: {{mainApiFriendlyName}} {{#if apiUsed}}({{apiUsed}}){{/if}} {{#if modelUsed}}&ndash; {{modelUsed}}{{/if}}<br>7<div>
8Tokenizer: {{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
9<span class="tokenItemizingSubclass">18<span class="tokenItemizingSubclass">
10 Only the white numbers really matter. All numbers are estimates.19 Only the white numbers really matter. All numbers are estimates.
11 Grey color items may not have been included in the context due to certain prompt format settings.20 Grey color items may not have been included in the context due to certain prompt format settings.
public/scripts/textgen-settings.js+3 -0
@@ -1340,6 +1340,9 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1340 'dry_sequence_breakers': sequenceBreakers,1340 'dry_sequence_breakers': sequenceBreakers,
1341 };1341 };
1342 params = Object.assign(params, llamaCppParams);1342 params = Object.assign(params, llamaCppParams);
1343 if (!Array.isArray(sequenceBreakers) || sequenceBreakers.length === 0) {
1344 delete params.dry_sequence_breakers;
1345 }
1343 }1346 }
13441347
1345 eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);1348 eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
src/endpoints/chats.js+69 -22
@@ -191,6 +191,44 @@ function importCAIChat(userName, characterName, jsonData) {
191}191}
192192
193/**193/**
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 */
200function 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/**
194 * Flattens `msg` and `swipes` data from Chub Chat format.232 * Flattens `msg` and `swipes` data from Chub Chat format.
195 * Only changes enough to make it compatible with the standard chat serialization format.233 * Only changes enough to make it compatible with the standard chat serialization format.
196 * @param {string} userName User name234 * @param {string} userName User name
@@ -413,7 +451,7 @@ router.post('/import', urlencodedParser, function (request, response) {
413 const format = request.body.file_type;451 const format = request.body.file_type;
414 const avatarUrl = (request.body.avatar_url).replace('.png', '');452 const avatarUrl = (request.body.avatar_url).replace('.png', '');
415 const characterName = request.body.character_name;453 const characterName = request.body.character_name;
416 const userName = request.body.user_name || 'You';454 const userName = request.body.user_name || 'User';
417455
418 if (!request.file) {456 if (!request.file) {
419 return response.sendStatus(400);457 return response.sendStatus(400);
@@ -426,33 +464,38 @@ router.post('/import', urlencodedParser, function (request, response) {
426 if (format === 'json') {464 if (format === 'json') {
427 fs.unlinkSync(pathToUpload);465 fs.unlinkSync(pathToUpload);
428 const jsonData = JSON.parse(data);466 const jsonData = JSON.parse(data);
429 if (jsonData.histories !== undefined) {467
430 // CAI Tools format468 /** @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
474 importFunc = importCAIChat;
475 } else if (Array.isArray(jsonData.data_visible)) { // oobabooga's format
476 importFunc = importOobaChat;
477 } else if (Array.isArray(jsonData.messages)) { // Agnai's format
478 importFunc = importAgnaiChat;
479 } else { // Unknown format
480 console.log('Incorrect chat format .json');
481 return response.send({ error: true });
436 }482 }
437 return response.send({ res: true });483
438 } else if (Array.isArray(jsonData.data_visible)) {484 const handleChat = (chat) => {
439 // oobabooga's format
440 const chat = importOobaChat(userName, characterName, jsonData);
441 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
442 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
443 writeFileAtomicSync(filePath, chat, 'utf8');
444 return response.send({ res: true });
445 } else if (Array.isArray(jsonData.messages)) {
446 // Agnai format
447 const chat = importAgnaiChat(userName, characterName, jsonData);
448 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;485 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
449 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);486 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
450 writeFileAtomicSync(filePath, chat, 'utf8');487 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);
452 } else {494 } else {
453 console.log('Incorrect chat format .json');495 handleChat(chat);
454 return response.send({ error: true });
455 }496 }
497
498 return response.send({ res: true });
456 }499 }
457500
458 if (format === 'jsonl') {501 if (format === 'jsonl') {
@@ -561,11 +604,15 @@ router.post('/search', jsonParser, function (request, response) {
561604
562 let targetGroup;605 let targetGroup;
563 for (const groupFile of groupFiles) {606 for (const groupFile of groupFiles) {
607 try {
564 const groupData = JSON.parse(fs.readFileSync(path.join(groupDir, groupFile), 'utf8'));608 const groupData = JSON.parse(fs.readFileSync(path.join(groupDir, groupFile), 'utf8'));
565 if (groupData.id === group_id) {609 if (groupData.id === group_id) {
566 targetGroup = groupData;610 targetGroup = groupData;
567 break;611 break;
568 }612 }
613 } catch (error) {
614 console.error(groupFile, 'group file is corrupted:', error);
615 }
569 }616 }
570617
571 if (!targetGroup?.chats) {618 if (!targetGroup?.chats) {