Compare commits

..

35 Commits

Author SHA1 Message Date
Cohee
1fa6d7d8ef Use ISO strings for message timestamps 2025-02-19 21:04:40 +02:00
Cohee
44182970bc Text Completion: Add context-specific {{timestamp}} macro 2025-02-18 22:12:32 +02:00
Cohee
1a4bcbb794 Remove input length restrictions from OpenAI compatible TTS
Closes #3505
2025-02-18 10:34:15 +02:00
Cohee
fa4a75215b Merge pull request #3497 from SillyTavern/fix-char-rename-aux-connections
Fix renaming characters losing connections of several aux fields
2025-02-17 21:14:41 +02:00
Cohee
0cdc389794 Fix type handling of active_character_id 2025-02-17 21:00:09 +02:00
Cohee
434bdab585 Merge branch 'staging' into fix-char-rename-aux-connections 2025-02-17 20:57:23 +02:00
Cohee
ce67f3a658 Ensure node is an Element in DOMPurify hook 2025-02-17 20:54:11 +02:00
Cohee
277fc00f38 Remove dompurify types stub 2025-02-17 20:53:42 +02:00
Cohee
36a3a7d615 Merge pull request #3486 from Yokayo/staging
Update ru-ru translation
2025-02-17 20:45:40 +02:00
Cohee
d15d49b295 Merge pull request #3484 from SillyTavern/reasoning-parsing-streaming
Implement reasoning parsing during streaming, based on provided prefix/suffix
2025-02-17 20:27:57 +02:00
Cohee
87afb1633b reasoning-set: Return if message not found 2025-02-17 19:54:50 +02:00
Cohee
e5e2c9fe49 Merge branch 'staging' into reasoning-parsing-streaming 2025-02-17 19:43:09 +02:00
Cohee
580856064e Merge pull request #3496 from SillyTavern/parse-reasoning-command-args
Add 'return' and 'strict' to `/reasoning-parse`, and make all reasoning parsing strict by default
2025-02-17 19:37:20 +02:00
Cohee
1a5b1f77d7 Merge pull request #3495 from SillyTavern/vectors-requests
Vectors: Don't use headers for source-specific fields in requests
2025-02-17 15:50:44 +02:00
Yokayo
f5d68f1cd9 Fix typo 2025-02-17 16:36:06 +07:00
Yokayo
2e77171137 Change key 2025-02-17 16:32:40 +07:00
Wolfsblvt
ab27b29819 cleanup group on auto load if name not found 2025-02-17 09:58:37 +01:00
Wolfsblvt
231068f729 await character renamed event 2025-02-17 09:47:35 +01:00
Wolfsblvt
7b65427236 typo 2025-02-17 09:19:08 +01:00
Wolfsblvt
9f21f7771c Auto-load char/group resets if target is not found 2025-02-17 07:28:59 +01:00
Wolfsblvt
1c0ca414b9 Update "active character" field on char rename 2025-02-17 05:34:28 +01:00
Wolfsblvt
4f0921856f On char rename, update auxiliary connections
- Move WI char lore (additional lorebooks) based on rename
- Move character-bound Author's Note based on rename
- Extend core `getCharFilename` to be able to take an avatarKey, instead of just uid
2025-02-17 05:32:13 +01:00
Wolfsblvt
994d69508b Merge branch 'parse-reasoning-command-args' into reasoning-parsing-streaming 2025-02-17 03:54:24 +01:00
Wolfsblvt
1ad3a2b968 Add 'return' and 'strict' to /reasoning-parse
- Add 'return' arg to `/reasoning-parse` to decide whether to return the reasoning, or the content without reasoning. Defaulting to reasoning.
- Add 'strict' arg to `/reasoning-parse` to decide whether the reasoning block has to be at the beginning of the provided message or not. Defaulting to true.
- Update parsing of all reasoning (even the auto parse one) to be strict by default - meaning the regex block has to be at the beginning (excluding whitespaces)
2025-02-17 03:47:31 +01:00
Cohee
cd2d6e85e0 Merge branch 'staging' into reasoning-parsing-streaming 2025-02-17 00:13:27 +02:00
Cohee
058c86f3c1 Vectors: Don't use headers for source-specific fields in requests 2025-02-16 23:59:00 +02:00
Cohee
a771dd5478 Show reasoning editor when hidden and empty 2025-02-16 23:21:08 +02:00
Cohee
baa4632071 Merge branch 'staging' into reasoning-parsing-streaming 2025-02-16 23:16:04 +02:00
Cohee
ebe877e6b6 Update dompurify 2025-02-16 23:14:50 +02:00
Cohee
1156b648eb Merge branch 'staging' into reasoning-parsing-streaming 2025-02-16 23:12:41 +02:00
Cohee
ec49b19aff Fix chat render when OpenRouter "website setting" model used 2025-02-16 22:46:44 +02:00
Yokayo
890d10d811 Work on tl 2025-02-16 18:43:57 +07:00
Wolfsblvt
b3688087d5 Save reasoning type with the message
- use mes extras property to save where the reasoning came from
- update it accordingly on streaming, slash commands and manual add
- Modify title tooltip on reasoning header to show the origin where it makes sense, providing the user with a little bit more orientation about the reasoning.
2025-02-16 05:05:48 +01:00
Wolfsblvt
bcea4248c4 Shouldn't parse reasoning if real reasoning there 2025-02-16 03:21:02 +01:00
Wolfsblvt
9590127bae Handle auto parsing reasoning during streaming
- Add function to handle auto parsing reasoning from the streamed message during streaming
- Only works when the reasoning prefix is EXACTLY at the beginning of the message
- Tried to keep this lightweight, no regex parsing, remembering the index, so it's simple string splicing
- Add utility function that trims a string only if `trim_spaces` is enabled
2025-02-16 03:09:20 +01:00
25 changed files with 539 additions and 266 deletions

28
package-lock.json generated
View File

@@ -28,7 +28,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"csrf-sync": "^4.0.3", "csrf-sync": "^4.0.3",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"dompurify": "^3.1.7", "dompurify": "^3.2.4",
"droll": "^0.2.1", "droll": "^0.2.1",
"express": "^4.21.0", "express": "^4.21.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@@ -87,7 +87,6 @@
"@types/cookie-session": "^2.0.49", "@types/cookie-session": "^2.0.49",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/deno": "^2.0.0", "@types/deno": "^2.0.0",
"@types/dompurify": "^3.0.5",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jquery": "^3.5.29", "@types/jquery": "^3.5.29",
"@types/jquery-cropper": "^1.0.4", "@types/jquery-cropper": "^1.0.4",
@@ -1181,16 +1180,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1478,8 +1467,8 @@
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true, "license": "MIT",
"license": "MIT" "optional": true
}, },
"node_modules/@types/write-file-atomic": { "node_modules/@types/write-file-atomic": {
"version": "4.0.3", "version": "4.0.3",
@@ -3236,10 +3225,13 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.1.7", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"license": "(MPL-2.0 OR Apache-2.0)" "license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
}, },
"node_modules/domutils": { "node_modules/domutils": {
"version": "3.1.0", "version": "3.1.0",

View File

@@ -18,7 +18,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"csrf-sync": "^4.0.3", "csrf-sync": "^4.0.3",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"dompurify": "^3.1.7", "dompurify": "^3.2.4",
"droll": "^0.2.1", "droll": "^0.2.1",
"express": "^4.21.0", "express": "^4.21.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@@ -116,7 +116,6 @@
"@types/cookie-session": "^2.0.49", "@types/cookie-session": "^2.0.49",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/deno": "^2.0.0", "@types/deno": "^2.0.0",
"@types/dompurify": "^3.0.5",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jquery": "^3.5.29", "@types/jquery": "^3.5.29",
"@types/jquery-cropper": "^1.0.4", "@types/jquery-cropper": "^1.0.4",

View File

@@ -2015,7 +2015,7 @@
</div> </div>
</div> </div>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom"> <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
<div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response."> <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
<label for="openai_reasoning_effort" data-i18n="Reasoning Effort"> <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
Reasoning Effort Reasoning Effort
</label> </label>
@@ -4001,7 +4001,7 @@
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions"> <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">
<small> <small>
<span data-i18n="Max Recursion Steps">Max Recursion Steps</span> <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
<div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div> <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>
</small> </small>
<input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1"> <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">
<input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter"> <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">
@@ -6890,8 +6890,8 @@
</div> </div>
<div id="form_sheld"> <div id="form_sheld">
<div id="dialogue_del_mes"> <div id="dialogue_del_mes">
<div id="dialogue_del_mes_ok" class="menu_button">Delete</div> <div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div>
<div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div> <div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div>
</div> </div>
<div id="send_form" class="no-connection"> <div id="send_form" class="no-connection">
<form id="file_form" class="wide100p displayNone"> <form id="file_form" class="wide100p displayNone">

View File

@@ -1485,7 +1485,7 @@
"(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)", "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",
"Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée", "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",
"Max Recursion Steps": "Nombre maximal d'étapes de récursivité", "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)", "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = illimité, 1 = scanne une fois et ne récure pas, 2 = scanne une fois et récure une fois, etc.\n(désactivé lorsque des activations minimales sont utilisées)",
"Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.", "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",
"Apply current sorting as Order": "Appliquer le tri actuel comme ordre", "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",
"Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.", "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",

View File

@@ -195,7 +195,7 @@
"Yes": "Да", "Yes": "Да",
"No": "Нет", "No": "Нет",
"Context %": "Процент контекста", "Context %": "Процент контекста",
"Budget Cap": "Бюджетный лимит", "Budget Cap": "Лимит бюджета",
"(0 = disabled)": "(0 = отключено)", "(0 = disabled)": "(0 = отключено)",
"None": "Отсутствует", "None": "Отсутствует",
"User Settings": "Настройки пользователя", "User Settings": "Настройки пользователя",
@@ -575,10 +575,10 @@
"Characters sorting order": "Порядок сортировки персонажей", "Characters sorting order": "Порядок сортировки персонажей",
"Remove": "Убрать", "Remove": "Убрать",
"Select a World Info file for": "Выбрать файл с миром для", "Select a World Info file for": "Выбрать файл с миром для",
"Primary Lorebook": "Основного лорбука", "Primary Lorebook": "Основной лорбук",
"A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук", "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.",
"When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира", "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.",
"Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON", "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.",
"Additional Lorebooks": "Вспомогательные лорбуки", "Additional Lorebooks": "Вспомогательные лорбуки",
"Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков", "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",
"NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!", "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",
@@ -593,7 +593,7 @@
"Prompt": "Промпт", "Prompt": "Промпт",
"Copy": "Скопировать", "Copy": "Скопировать",
"Confirm": "Подтвердить", "Confirm": "Подтвердить",
"Copy this message": "Скопировать сообщение", "Copy this message": "Продублировать сообщение",
"Delete this message": "Удалить сообщение", "Delete this message": "Удалить сообщение",
"Move message up": "Переместить сообщение вверх", "Move message up": "Переместить сообщение вверх",
"Move message down": "Переместить сообщение вниз", "Move message down": "Переместить сообщение вниз",
@@ -612,7 +612,7 @@
"Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас", "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",
"Continue the last message": "Продолжить текущее сообщение", "Continue the last message": "Продолжить текущее сообщение",
"Bind user name to that avatar": "Закрепить имя за этим аватаром", "Bind user name to that avatar": "Закрепить имя за этим аватаром",
"Select this as default persona for the new chats.": "Выберать эту Персону в качестве персоны по умолчанию для новых чатов.", "Select this as default persona for the new chats.": "Выбирать эту персону по умолчанию для всех новых чатов.",
"Change persona image": "Сменить аватар персоны", "Change persona image": "Сменить аватар персоны",
"Delete persona": "Удалить персону", "Delete persona": "Удалить персону",
"Reduced Motion": "Сокращение анимаций", "Reduced Motion": "Сокращение анимаций",
@@ -640,7 +640,7 @@
"Token Probabilities": "Вероятности токенов", "Token Probabilities": "Вероятности токенов",
"Close chat": "Закрыть чат", "Close chat": "Закрыть чат",
"Manage chat files": "Все чаты", "Manage chat files": "Все чаты",
"Import Extension From Git Repo": "Импортировать расширение из Git Repository", "Import Extension From Git Repo": "Импортировать расширение из Git-репозитория.",
"Install extension": "Установить расширение", "Install extension": "Установить расширение",
"Manage extensions": "Управление расширениями", "Manage extensions": "Управление расширениями",
"Tokens persona description": "Токенов", "Tokens persona description": "Токенов",
@@ -1122,7 +1122,7 @@
"help_hotkeys_0": "Горячие клавиши", "help_hotkeys_0": "Горячие клавиши",
"You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню", "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",
"Download Extensions & Assets": "Загрузить расширения и ресурсы", "Download Extensions & Assets": "Загрузить расширения и ресурсы",
"menu within": нутри этих кубиков", "menu within": меню",
"Assets URL": "URL с описанием ресурсов", "Assets URL": "URL с описанием ресурсов",
"Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)", "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",
"Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)", "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",
@@ -1943,7 +1943,7 @@
"and connect to an": "и подключитесь к", "and connect to an": "и подключитесь к",
"You can add more": "Можете добавить больше", "You can add more": "Можете добавить больше",
"from other websites": "с других сайтов.", "from other websites": "с других сайтов.",
"Go to the": "Загляните в", "Go to the": "Заходите в",
"to install additional features.": ", чтобы установить разные дополнительные ресурсы.", "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
"or_welcome": "; также доступен", "or_welcome": "; также доступен",
"Claude API Key": "Ключ от API Claude", "Claude API Key": "Ключ от API Claude",
@@ -1958,7 +1958,7 @@
"Save": "Сохранить", "Save": "Сохранить",
"Chat Lorebook": "Лорбук для чата", "Chat Lorebook": "Лорбук для чата",
"chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.", "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",
"world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + клик, чтобы открыть диалог привязки мира", "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + ЛКМ, чтобы открыть диалог привязки мира",
"No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.", "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
"ext_regex_user_input_desc": "Отправленные вами сообщения.", "ext_regex_user_input_desc": "Отправленные вами сообщения.",
"ext_regex_ai_input_desc": "Полученные от API ответы.", "ext_regex_ai_input_desc": "Полученные от API ответы.",
@@ -2144,5 +2144,65 @@
"Not connected to the API!": "Нет соединения с API!", "Not connected to the API!": "Нет соединения с API!",
"ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.", "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
"Update all": "Обновить все", "Update all": "Обновить все",
"Close": "Закрыть" "Close": "Закрыть",
"Optional modules:": "Необязательные модули:",
"Sort: Display Name": "Сортировать: по названию",
"Sort: Loading Order": "Сортировать: в порядке загрузки",
"Click to toggle": "Нажмите, чтобы включить или выключить",
"Loading Asset List": "Загрузить список ресурсов",
"Don't ask again for this URL": "Запомнить выбор для этого адреса",
"Are you sure you want to connect to the following url?": "Вы точно хотите подключиться к этому адресу?",
"All": "Всё",
"Characters": "Персонажи",
"Ambient sounds": "Звуковой эмбиент",
"Blip sounds": "Звуки уведомлений",
"Background music": "Фоновая музыка",
"Search": "Поиск",
"extension_install_1": "Чтобы загружать расширения из этого списка, у вас должен быть установлен ",
"extension_install_2": ".",
"extension_install_3": "Нажмите на иконку ",
"extension_install_4": ", чтобы перейти в репозиторий расширения и получить более подробную информацию о нём.",
"Extension repo/guide:": "Репозиторий расширения:",
"Preview in browser": "Предпросмотр",
"Adds a function tool": "Частично или полностью работает через вызов функций",
"Tool": "Функции",
"Move extension": "Переместить расширение",
"ext_type_local": "Это локальное расширение, доступно только вам",
"ext_type_global": "Это глобальное расширение, доступно всем пользователям",
"Move": "Переместить",
"Enter the Git URL of the extension to install": "Введите Git-адрес расширения",
"Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.": "помните, что используя расширения от сторонних авторов, вы можете подвергать систему опасности. Устанавливайте расширения только от проверенных разработчиков. Мы не несём ответственности за любой ущерб, причинённый сторонними расширениями.",
"Disclaimer:": "Внимание:",
"Example:": "Пример:",
"context_derived": "Считывать из метаданных модели (по возможности)",
"instruct_derived": "Считывать из метаданных модели (по возможности)",
"Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
"Reasoning Effort": "Рассуждения",
"Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
"openai_reasoning_effort_low": "Поверхностные",
"openai_reasoning_effort_medium": "Обычные",
"openai_reasoning_effort_high": "Подробные",
"Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук",
"Persona Lorebook for": "Лорбук для персоны",
"persona_world_template_txt": "Выбранная Информация о мире будет привязана к этой персоне. Информация будет добавляться в каждом промпте вместе с глобальным лорбуком и лорбуками персонажа и чата.",
"Global list": "Глобальный список",
"Preset-specific list": "Список для данного пресета",
"Banned tokens/strings are being sent in the request.": "Запрещённые токены и строки отсылаются в запросе.",
"Banned tokens/strings are NOT being sent in the request.": "Запрещённые токены и строки НЕ отсылаются в запросе.",
"Add a reasoning block": "Добавить блок рассуждений",
"Create a copy of this message?": "Продублировать это сообщение?",
"Max Recursion Steps": "Макс. глубина рекурсии",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = неограничено, 1 = сканировать единожды, 2 = сканировать единожды и сделать один повторный проход, и т.д.\n(неактивно при указанном мин. числе активаций)",
"(disabled when max recursion steps are used)": "(неактивно при указанной макс. глубине рекурсии)",
"Enter a valid API URL": "Введите корректный адрес API",
"No Ollama model selected.": "Не выбрана модель Ollama",
"Background Fitting": "Способ подгонки фона под разрешение",
"Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук",
"Token Counter": "Подсчитать токены",
"Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",
"Selected tokenizer:": "Выбранный токенайзер:",
"Input:": "Входные данные:",
"Tokenized text:": "Токенизированный текст:",
"Token IDs:": "Идентификаторы токенов:",
"Tokens:": "Токенов:"
} }

View File

@@ -584,7 +584,7 @@
"(0 = unlimited, use budget)": "“0”为无限制使用预算", "(0 = unlimited, use budget)": "“0”为无限制使用预算",
"Cap the number of entry activation recursions": "限制条目激活递归的次数", "Cap the number of entry activation recursions": "限制条目激活递归的次数",
"Max Recursion Steps": "最大递归深度", "Max Recursion Steps": "最大递归深度",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "“0”为无限制“1”为扫描一次且不递归“2”为扫描一次且递归一次依此类推\n当使用最小激活次数时此功能被禁用", "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "“0”为无限制“1”为扫描一次且不递归“2”为扫描一次且递归一次依此类推\n当使用最小激活次数时此功能被禁用",
"Insertion Strategy": "插入策略", "Insertion Strategy": "插入策略",
"Sorted Evenly": "均匀排序", "Sorted Evenly": "均匀排序",
"Character Lore First": "角色世界书优先", "Character Lore First": "角色世界书优先",

View File

@@ -1458,7 +1458,7 @@
"Example: http://localhost:1234/v1": "例如http://localhost:1234/v1", "Example: http://localhost:1234/v1": "例如http://localhost:1234/v1",
"popup-button-crop": "裁剪", "popup-button-crop": "裁剪",
"(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)", "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制1 = 掃描一次且不遞歸2 = 掃描一次並遞歸一次,以此類推\n使用最小啟動設定時將停用", "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制1 = 掃描一次且不遞歸2 = 掃描一次並遞歸一次,以此類推\n使用最小啟動設定時將停用",
"A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列並在每一步中保持固定數量的頂級序列beam width。", "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列並在每一步中保持固定數量的頂級序列beam width。",
"A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。", "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",
"Abort current image generation task": "終止目前的圖片生成任務", "Abort current image generation task": "終止目前的圖片生成任務",
@@ -1806,7 +1806,7 @@
"context_derived": "若可能,根據模型元數據推導。", "context_derived": "若可能,根據模型元數據推導。",
"instruct_derived": "若可能,根據模型元數據推導。", "instruct_derived": "若可能,根據模型元數據推導。",
"Inserted before the first User's message.": "插入於第一則使用者訊息之前。", "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",
"0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\\n(disabled when min activations are used)": "0 = 無限制1 = 掃描一次不遞歸2 = 掃描一次後遞歸一次 ⋯以此類推\n啟用最小啟動次數時無效", "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制1 = 掃描一次不遞歸2 = 掃描一次後遞歸一次 ⋯以此類推\n啟用最小啟動次數時無效",
"Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕", "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",
"Manual": "手動", "Manual": "手動",
"Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示Post-History Instructions。\nv2 格式specpost_history_instructions", "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示Post-History Instructions。\nv2 格式specpost_history_instructions",

View File

@@ -366,6 +366,10 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
return; return;
} }
if (!(node instanceof Element)) {
return;
}
let mediaBlocked = false; let mediaBlocked = false;
switch (node.tagName) { switch (node.tagName) {
@@ -493,6 +497,7 @@ export const event_types = {
// TODO: Naming convention is inconsistent with other events // TODO: Naming convention is inconsistent with other events
CHARACTER_DELETED: 'characterDeleted', CHARACTER_DELETED: 'characterDeleted',
CHARACTER_DUPLICATED: 'character_duplicated', CHARACTER_DUPLICATED: 'character_duplicated',
CHARACTER_RENAMED: 'character_renamed',
/** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */ /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received', SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
STREAM_TOKEN_RECEIVED: 'stream_token_received', STREAM_TOKEN_RECEIVED: 'stream_token_received',
@@ -1027,12 +1032,22 @@ export function setAnimationDuration(ms = null) {
document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`); document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
} }
/**
* Sets the currently active character
* @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active character is reset to `null`.
*/
export function setActiveCharacter(entityOrKey) { export function setActiveCharacter(entityOrKey) {
active_character = getTagKeyForEntity(entityOrKey); active_character = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
if (active_character) active_group = null;
} }
/**
* Sets the currently active group.
* @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active group is reset to `null`.
*/
export function setActiveGroup(entityOrKey) { export function setActiveGroup(entityOrKey) {
active_group = getTagKeyForEntity(entityOrKey); active_group = entityOrKey ? getTagKeyForEntity(entityOrKey) : null;
if (active_group) active_character = null;
} }
/** /**
@@ -3223,6 +3238,7 @@ class StreamingProcessor {
// Update reasoning // Update reasoning
await this.reasoningHandler.process(messageId, mesChanged); await this.reasoningHandler.process(messageId, mesChanged);
processedText = chat[messageId]['mes'];
// Token count update. // Token count update.
const tokenCountText = this.reasoningHandler.reasoning + processedText; const tokenCountText = this.reasoningHandler.reasoning + processedText;
@@ -3373,7 +3389,7 @@ class StreamingProcessor {
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
} }
// Get the updated reasoning string into the handler // Get the updated reasoning string into the handler
this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? ''); this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text)); await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
} }
@@ -3413,6 +3429,15 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0; const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;
const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride; const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride;
const isQuiet = true; const isQuiet = true;
const getInstructFormatParams = (/** @type {string} */ mes) => ({
name: name1,
mes: mes,
isUser: false,
isNarrator: true,
forceAvatar: '',
forceOutputSequence: false,
timestamp: moment(),
});
let eventHook = () => { }; let eventHook = () => { };
if (systemPrompt) { if (systemPrompt) {
@@ -3423,8 +3448,8 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
prompt = substituteParams(prompt); prompt = substituteParams(prompt);
prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt; prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt;
prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt; prompt = isInstruct ? formatInstructModeChat(getInstructFormatParams(prompt)) : prompt;
prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2, isQuiet, quietToLoud)) : (prompt + '\n'); prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', isQuiet, quietToLoud)) : (prompt + '\n');
try { try {
if (responseLengthCustomized) { if (responseLengthCustomized) {
@@ -4000,7 +4025,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
const mesExamplesRawArray = [...mesExamplesArray]; const mesExamplesRawArray = [...mesExamplesArray];
if (mesExamplesArray && isInstruct) { if (mesExamplesArray && isInstruct) {
mesExamplesArray = formatInstructModeExamples(mesExamplesArray, name1, name2); mesExamplesArray = formatInstructModeExamples(mesExamplesArray);
} }
if (skipWIAN !== true) { if (skipWIAN !== true) {
@@ -4320,7 +4345,16 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
// here name1 is forced for all quiet prompts..why? // here name1 is forced for all quiet prompts..why?
const name = name1; const name = name1;
//checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt //checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt
const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, false, true, '', name1, name2, false) : `\n${quiet_prompt}`; const quietInstructParams = {
name: name,
mes: quiet_prompt,
isUser: false,
isNarrator: true,
forceAvatar: '',
forceOutputSequence: false,
timestamp: moment(),
};
const quietAppend = isInstruct ? formatInstructModeChat(quietInstructParams) : `\n${quiet_prompt}`;
//This begins to fix quietPrompts (particularly /sysgen) for instruct //This begins to fix quietPrompts (particularly /sysgen) for instruct
//previously instruct input sequence was being appended to the last chat message w/o '\n' //previously instruct input sequence was being appended to the last chat message w/o '\n'
@@ -4350,7 +4384,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (isInstruct && !isContinue) { if (isInstruct && !isContinue) {
const name = (quiet_prompt && !quietToLoud && !isImpersonate) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2); const name = (quiet_prompt && !quietToLoud && !isImpersonate) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
const isQuiet = quiet_prompt && type == 'quiet'; const isQuiet = quiet_prompt && type == 'quiet';
lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud); lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, isQuiet, quietToLoud);
} }
// Get non-instruct impersonation line // Get non-instruct impersonation line
@@ -5131,7 +5165,16 @@ function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) {
let textResult = chatItem?.name && shouldPrependName ? `${itemName}: ${chatItem.mes}\n` : `${chatItem.mes}\n`; let textResult = chatItem?.name && shouldPrependName ? `${itemName}: ${chatItem.mes}\n` : `${chatItem.mes}\n`;
if (isInstruct) { if (isInstruct) {
textResult = formatInstructModeChat(itemName, chatItem.mes, chatItem.is_user, isNarratorType, chatItem.force_avatar, name1, name2, forceOutputSequence); const instructFormatParams = {
name: itemName,
mes: chatItem.mes,
isUser: chatItem.is_user,
isNarrator: isNarratorType,
forceAvatar: chatItem.force_avatar,
forceOutputSequence: forceOutputSequence,
timestamp: timestampToMoment(chatItem.send_date),
};
textResult = formatInstructModeChat(instructFormatParams);
} }
return textResult; return textResult;
@@ -6241,9 +6284,35 @@ export async function renameCharacter(name = null, { silent = false, renameChats
const data = await response.json(); const data = await response.json();
const newAvatar = data.avatar; const newAvatar = data.avatar;
// Replace tags list const oldName = getCharaFilename(null, { manualAvatarKey: oldAvatar });
const newName = getCharaFilename(null, { manualAvatarKey: newAvatar });
// Replace other auxillery fields where was referenced by avatar key
// Tag List
renameTagKey(oldAvatar, newAvatar); renameTagKey(oldAvatar, newAvatar);
// Addtional lore books
const charLore = world_info.charLore?.find(x => x.name == oldName);
if (charLore) {
charLore.name = newName;
saveSettingsDebounced();
}
// Char-bound Author's Notes
const charNote = extension_settings.note.chara?.find(x => x.name == oldName);
if (charNote) {
charNote.name = newName;
saveSettingsDebounced();
}
// Update active character, if the current one was the currently active one
if (active_character === oldAvatar) {
active_character = newAvatar;
saveSettingsDebounced();
}
await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
// Reload characters list // Reload characters list
await getCharacters(); await getCharacters();
@@ -10947,7 +11016,7 @@ jQuery(async function () {
}); });
$(document).on('click', '.mes_edit_copy', async function () { $(document).on('click', '.mes_edit_copy', async function () {
const confirmation = await callGenericPopup('Create a copy of this message?', POPUP_TYPE.CONFIRM); const confirmation = await callGenericPopup(t`Create a copy of this message?`, POPUP_TYPE.CONFIRM);
if (!confirmation) { if (!confirmation) {
return; return;
} }

View File

@@ -177,29 +177,13 @@ export function humanizedDateTime() {
return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s`; return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s`;
} }
//this is a common format version to display a timestamp on each chat message /**
//returns something like: June 19, 2023 2:20pm * Gets the current date and time for message timestamps.
* @returns {string} - A string representing the current date and time in ISO format.
*/
export function getMessageTimeStamp() { export function getMessageTimeStamp() {
const date = Date.now(); return new Date().toISOString();
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const d = new Date(date);
const month = months[d.getMonth()];
const day = d.getDate();
const year = d.getFullYear();
let hours = d.getHours();
const minutes = ('0' + d.getMinutes()).slice(-2);
let meridiem = 'am';
if (hours >= 12) {
meridiem = 'pm';
hours -= 12;
} }
if (hours === 0) {
hours = 12;
}
const formattedDate = month + ' ' + day + ', ' + year + ' ' + hours + ':' + minutes + meridiem;
return formattedDate;
}
// triggers: // triggers:
$('#rm_button_create').on('click', function () { //when "+New Character" is clicked $('#rm_button_create').on('click', function () { //when "+New Character" is clicked
@@ -280,17 +264,32 @@ async function RA_autoloadchat() {
// active character is the name, we should look it up in the character list and get the id // active character is the name, we should look it up in the character list and get the id
if (active_character !== null && active_character !== undefined) { if (active_character !== null && active_character !== undefined) {
const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character); const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
if (active_character_id !== null) { if (active_character_id !== -1) {
await selectCharacterById(String(active_character_id)); await selectCharacterById(String(active_character_id));
// Do a little tomfoolery to spoof the tag selector // Do a little tomfoolery to spoof the tag selector
const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`); const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
applyTagsOnCharacterSelect.call(selectedCharElement); applyTagsOnCharacterSelect.call(selectedCharElement);
} else {
setActiveCharacter(null);
saveSettingsDebounced();
console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
} }
} }
if (active_group !== null && active_group !== undefined) { if (active_group !== null && active_group !== undefined) {
await openGroupById(String(active_group)); if (active_character) {
console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
setActiveGroup(null);
saveSettingsDebounced();
} else {
const result = await openGroupById(String(active_group));
if (!result) {
setActiveGroup(null);
saveSettingsDebounced();
console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
}
}
} }
// if the character list hadn't been loaded yet, try again. // if the character list hadn't been loaded yet, try again.

View File

@@ -603,12 +603,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
} }
let toggleElement = isActive || isDisabled ? let toggleElement = isActive || isDisabled ?
`<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` : '<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
`<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`; `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : ''; let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : ''; let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : ''; let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
let modulesInfo = ''; let modulesInfo = '';
if (isActive && Array.isArray(manifest.optional)) { if (isActive && Array.isArray(manifest.optional)) {
@@ -616,7 +616,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
modules.forEach(x => optional.delete(x)); modules.forEach(x => optional.delete(x));
if (optional.size > 0) { if (optional.size > 0) {
const optionalString = DOMPurify.sanitize([...optional].join(', ')); const optionalString = DOMPurify.sanitize([...optional].join(', '));
modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`; modulesInfo = '<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
} }
} else if (!isDisabled) { // Neither active nor disabled } else if (!isDisabled) { // Neither active nor disabled
const requirements = new Set(manifest.requires); const requirements = new Set(manifest.requires);

View File

@@ -10,6 +10,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
import { executeSlashCommands } from '../../slash-commands.js'; import { executeSlashCommands } from '../../slash-commands.js';
import { accountStorage } from '../../util/AccountStorage.js'; import { accountStorage } from '../../util/AccountStorage.js';
import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js'; import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
import { t } from '../../i18n.js';
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'assets'; const MODULE_NAME = 'assets';
@@ -59,11 +60,11 @@ const KNOWN_TYPES = {
'blip': 'Blip sounds', 'blip': 'Blip sounds',
}; };
function downloadAssetsList(url) { async function downloadAssetsList(url) {
updateCurrentAssets().then(function () { updateCurrentAssets().then(async function () {
fetch(url, { cache: 'no-cache' }) fetch(url, { cache: 'no-cache' })
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(async function(json) {
availableAssets = {}; availableAssets = {};
$('#assets_menu').empty(); $('#assets_menu').empty();
@@ -84,10 +85,10 @@ function downloadAssetsList(url) {
$('#assets_type_select').empty(); $('#assets_type_select').empty();
$('#assets_search').val(''); $('#assets_search').val('');
$('#assets_type_select').append($('<option />', { value: '', text: 'All' })); $('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
for (const type of assetTypes) { for (const type of assetTypes) {
const option = $('<option />', { value: type, text: KNOWN_TYPES[type] || type }); const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
$('#assets_type_select').append(option); $('#assets_type_select').append(option);
} }
@@ -104,11 +105,7 @@ function downloadAssetsList(url) {
assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide(); assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
if (assetType == 'extension') { if (assetType == 'extension') {
assetTypeMenu.append(` assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
<div class="assets-list-git">
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it.
</div>`);
} }
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) { for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
@@ -184,7 +181,7 @@ function downloadAssetsList(url) {
const displayName = DOMPurify.sanitize(asset['name'] || asset['id']); const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
const description = DOMPurify.sanitize(asset['description'] || ''); const description = DOMPurify.sanitize(asset['description'] || '');
const url = isValidUrl(asset['url']) ? asset['url'] : ''; const url = isValidUrl(asset['url']) ? asset['url'] : '';
const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser'; const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
const toolTag = assetType === 'extension' && asset['tool']; const toolTag = assetType === 'extension' && asset['tool'];
@@ -195,9 +192,10 @@ function downloadAssetsList(url) {
<b>${displayName}</b> <b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="${title}"> <a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i> <i class="fa-solid fa-sm ${previewIcon}"></i>
</a> </a>` +
${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''} (toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' +
</span> t`Tool` + '</span>' : '') +
`</span>
<small class="asset-description"> <small class="asset-description">
${description} ${description}
</small> </small>
@@ -435,7 +433,7 @@ jQuery(async () => {
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`; const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
const skipConfirm = accountStorage.getItem(rememberKey) === 'true'; const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, { const confirmation = skipConfirm || await Popup.show.confirm(t`Loading Asset List`, '<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${url}</var>`, {
customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }], customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
onClose: popup => { onClose: popup => {
if (popup.result) { if (popup.result) {

View File

@@ -0,0 +1,4 @@
<div class="assets-list-git">
<span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br>
<span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span>
</div>

View File

@@ -33,7 +33,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
<div id="assets_filters" class="flex-container"> <div id="assets_filters" class="flex-container">
<select id="assets_type_select" class="text_pole flex1"> <select id="assets_type_select" class="text_pole flex1">
</select> </select>
<input id="assets_search" class="text_pole flex1" placeholder="Search" type="search"> <input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
<div id="assets-characters-button" class="menu_button menu_button_icon"> <div id="assets-characters-button" class="menu_button menu_button_icon">
<i class="fa-solid fa-image-portrait"></i> <i class="fa-solid fa-image-portrait"></i>
<span data-i18n="Characters">Characters</span> <span data-i18n="Characters">Characters</span>

View File

@@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
import { resetScrollHeight, debounce } from '../../utils.js'; import { resetScrollHeight, debounce } from '../../utils.js';
import { debounce_timeout } from '../../constants.js'; import { debounce_timeout } from '../../constants.js';
import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { renderExtensionTemplateAsync } from '../../extensions.js';
import { t } from '../../i18n.js';
function rgb2hex(rgb) { function rgb2hex(rgb) {
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@@ -22,23 +24,7 @@ $('button').click(function () {
async function doTokenCounter() { async function doTokenCounter() {
const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api); const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
const html = ` const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
<div class="wide100p">
<h3>Token Counter</h3>
<div class="justifyLeft flex-container flexFlowColumn">
<h4>Type / paste in the box below to see the number of tokens in the text.</h4>
<p>Selected tokenizer: ${tokenizerName}</p>
<div>Input:</div>
<textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
<div>Tokens: <span id="token_counter_result">0</span></div>
<hr>
<div>Tokenized text:</div>
<div id="tokenized_chunks_display" class="wide100p">—</div>
<hr>
<div>Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
</div>
</div>`;
const dialog = $(html); const dialog = $(html);
const countDebounced = debounce(async () => { const countDebounced = debounce(async () => {
@@ -131,9 +117,9 @@ async function doCount() {
jQuery(() => { jQuery(() => {
const buttonHtml = ` const buttonHtml = `
<div id="token_counter" class="list-group-item flex-container flexGap5"> <div id="token_counter" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div> <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
Token Counter t`Token Counter` +
</div>`; '</div>';
$('#token_counter_wand_container').append(buttonHtml); $('#token_counter_wand_container').append(buttonHtml);
$('#token_counter').on('click', doTokenCounter); $('#token_counter').on('click', doTokenCounter);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({

View File

@@ -0,0 +1,16 @@
<div class="wide100p">
<h3 data-i18n="Token Counter">Token Counter</h3>
<div class="justifyLeft flex-container flexFlowColumn">
<h4 data-i18n="Type / paste in the box below to see the number of tokens in the text.">Type / paste in the box below to see the number of tokens in the text.</h4>
<p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p>
<div data-i18n="Input:">Input:</div>
<textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
<div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div>
<hr>
<div data-i18n="Tokenized text:">Tokenized text:</div>
<div id="tokenized_chunks_display" class="wide100p"></div>
<hr>
<div data-i18n="Token IDs:">Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1"></textarea>
</div>
</div>

View File

@@ -25,7 +25,7 @@ class OpenAICompatibleTtsProvider {
<label for="openai_compatible_tts_endpoint">Provider Endpoint:</label> <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
<div class="flex-container alignItemsCenter"> <div class="flex-container alignItemsCenter">
<div class="flex1"> <div class="flex1">
<input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/> <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
</div> </div>
<div id="openai_compatible_tts_key" class="menu_button menu_button_icon"> <div id="openai_compatible_tts_key" class="menu_button menu_button_icon">
<i class="fa-solid fa-key"></i> <i class="fa-solid fa-key"></i>
@@ -33,9 +33,9 @@ class OpenAICompatibleTtsProvider {
</div> </div>
</div> </div>
<label for="openai_compatible_model">Model:</label> <label for="openai_compatible_model">Model:</label>
<input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/> <input id="openai_compatible_model" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.model}"/>
<label for="openai_compatible_tts_voices">Available Voices (comma separated):</label> <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
<input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/> <input id="openai_compatible_tts_voices" type="text" class="text_pole" value="${this.defaultSettings.available_voices.join()}"/>
<label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label> <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
<input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`; <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
return html; return html;

View File

@@ -745,6 +745,44 @@ async function getQueryText(chat, initiator) {
return collapseNewlines(queryText).trim(); return collapseNewlines(queryText).trim();
} }
/**
* Gets common body parameters for vector requests.
* @returns {object}
*/
function getVectorsRequestBody() {
const body = {};
switch (settings.source) {
case 'extras':
body.extrasUrl = extension_settings.apiUrl;
body.extrasKey = extension_settings.apiKey;
break;
case 'togetherai':
body.model = extension_settings.vectors.togetherai_model;
break;
case 'openai':
body.model = extension_settings.vectors.openai_model;
break;
case 'cohere':
body.model = extension_settings.vectors.cohere_model;
break;
case 'ollama':
body.model = extension_settings.vectors.ollama_model;
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
body.keep = !!extension_settings.vectors.ollama_keep;
break;
case 'llamacpp':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
break;
case 'vllm':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
body.model = extension_settings.vectors.vllm_model;
break;
default:
break;
}
return body;
}
/** /**
* Gets the saved hashes for a collection * Gets the saved hashes for a collection
* @param {string} collectionId * @param {string} collectionId
@@ -753,8 +791,9 @@ async function getQueryText(chat, initiator) {
async function getSavedHashes(collectionId) { async function getSavedHashes(collectionId) {
const response = await fetch('/api/vector/list', { const response = await fetch('/api/vector/list', {
method: 'POST', method: 'POST',
headers: getVectorHeaders(), headers: getRequestHeaders(),
body: JSON.stringify({ body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId, collectionId: collectionId,
source: settings.source, source: settings.source,
}), }),
@@ -768,54 +807,6 @@ async function getSavedHashes(collectionId) {
return hashes; return hashes;
} }
function getVectorHeaders() {
const headers = getRequestHeaders();
switch (settings.source) {
case 'extras':
Object.assign(headers, {
'X-Extras-Url': extension_settings.apiUrl,
'X-Extras-Key': extension_settings.apiKey,
});
break;
case 'togetherai':
Object.assign(headers, {
'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
});
break;
case 'openai':
Object.assign(headers, {
'X-OpenAI-Model': extension_settings.vectors.openai_model,
});
break;
case 'cohere':
Object.assign(headers, {
'X-Cohere-Model': extension_settings.vectors.cohere_model,
});
break;
case 'ollama':
Object.assign(headers, {
'X-Ollama-Model': extension_settings.vectors.ollama_model,
'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],
'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
});
break;
case 'llamacpp':
Object.assign(headers, {
'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
});
break;
case 'vllm':
Object.assign(headers, {
'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
'X-Vllm-Model': extension_settings.vectors.vllm_model,
});
break;
default:
break;
}
return headers;
}
/** /**
* Inserts vector items into a collection * Inserts vector items into a collection
* @param {string} collectionId - The collection to insert into * @param {string} collectionId - The collection to insert into
@@ -825,12 +816,11 @@ function getVectorHeaders() {
async function insertVectorItems(collectionId, items) { async function insertVectorItems(collectionId, items) {
throwIfSourceInvalid(); throwIfSourceInvalid();
const headers = getVectorHeaders();
const response = await fetch('/api/vector/insert', { const response = await fetch('/api/vector/insert', {
method: 'POST', method: 'POST',
headers: headers, headers: getRequestHeaders(),
body: JSON.stringify({ body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId, collectionId: collectionId,
items: items, items: items,
source: settings.source, source: settings.source,
@@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
async function deleteVectorItems(collectionId, hashes) { async function deleteVectorItems(collectionId, hashes) {
const response = await fetch('/api/vector/delete', { const response = await fetch('/api/vector/delete', {
method: 'POST', method: 'POST',
headers: getVectorHeaders(), headers: getRequestHeaders(),
body: JSON.stringify({ body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId, collectionId: collectionId,
hashes: hashes, hashes: hashes,
source: settings.source, source: settings.source,
@@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
* @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
*/ */
async function queryCollection(collectionId, searchText, topK) { async function queryCollection(collectionId, searchText, topK) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query', { const response = await fetch('/api/vector/query', {
method: 'POST', method: 'POST',
headers: headers, headers: getRequestHeaders(),
body: JSON.stringify({ body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId, collectionId: collectionId,
searchText: searchText, searchText: searchText,
topK: topK, topK: topK,
@@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
* @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
*/ */
async function queryMultipleCollections(collectionIds, searchText, topK, threshold) { async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query-multi', { const response = await fetch('/api/vector/query-multi', {
method: 'POST', method: 'POST',
headers: headers, headers: getRequestHeaders(),
body: JSON.stringify({ body: JSON.stringify({
...getVectorsRequestBody(),
collectionIds: collectionIds, collectionIds: collectionIds,
searchText: searchText, searchText: searchText,
topK: topK, topK: topK,
@@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
const response = await fetch('/api/vector/purge', { const response = await fetch('/api/vector/purge', {
method: 'POST', method: 'POST',
headers: getVectorHeaders(), headers: getRequestHeaders(),
body: JSON.stringify({ body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId, collectionId: collectionId,
}), }),
}); });
@@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
const response = await fetch('/api/vector/purge', { const response = await fetch('/api/vector/purge', {
method: 'POST', method: 'POST',
headers: getVectorHeaders(), headers: getRequestHeaders(),
body: JSON.stringify({ body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId, collectionId: collectionId,
}), }),
}); });
@@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
try { try {
const response = await fetch('/api/vector/purge-all', { const response = await fetch('/api/vector/purge-all', {
method: 'POST', method: 'POST',
headers: getVectorHeaders(), headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
}),
}); });
if (!response.ok) { if (!response.ok) {

View File

@@ -1664,12 +1664,12 @@ function updateFavButtonState(state) {
export async function openGroupById(groupId) { export async function openGroupById(groupId) {
if (isChatSaving) { if (isChatSaving) {
toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`); toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return; return false;
} }
if (!groups.find(x => x.id === groupId)) { if (!groups.find(x => x.id === groupId)) {
console.log('Group not found', groupId); console.log('Group not found', groupId);
return; return false;
} }
if (!is_send_press && !is_group_generating) { if (!is_send_press && !is_group_generating) {
@@ -1686,8 +1686,11 @@ export async function openGroupById(groupId) {
updateChatMetadata({}, true); updateChatMetadata({}, true);
chat.length = 0; chat.length = 0;
await getGroupChat(groupId); await getGroupChat(groupId);
return true;
} }
} }
return false;
} }
function openCharacterDefinition(characterSelect) { function openCharacterDefinition(characterSelect) {

View File

@@ -8,6 +8,7 @@ import {
context_presets, context_presets,
} from './power-user.js'; } from './power-user.js';
import { regexFromString, resetScrollHeight } from './utils.js'; import { regexFromString, resetScrollHeight } from './utils.js';
import { moment } from '../lib.js';
/** /**
* @type {any[]} Instruct mode presets. * @type {any[]} Instruct mode presets.
@@ -254,8 +255,10 @@ export function getInstructStoppingSequences() {
// Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string // Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string
// But it's a problem for Metharme which doesn't use newlines to separate them. // But it's a problem for Metharme which doesn't use newlines to separate them.
const wrap = (s) => power_user.instruct.wrap ? '\n' + s : s; const wrap = (s) => power_user.instruct.wrap ? '\n' + s : s;
// Should not contain timestamp macros
const hasTimestamp = /{{timestamp}}/gi.test(sequence) || /{{timestamp::(.*?)}}/gi.test(sequence);
// Sequence must be a non-empty string // Sequence must be a non-empty string
if (typeof sequence === 'string' && sequence.length > 0) { if (typeof sequence === 'string' && sequence.length > 0 && !hasTimestamp) {
// If sequence is just a whitespace or newline - we don't want to make it a stopping string // If sequence is just a whitespace or newline - we don't want to make it a stopping string
// User can always add it as a custom stop string if really needed // User can always add it as a custom stop string if really needed
if (sequence.trim().length > 0) { if (sequence.trim().length > 0) {
@@ -309,19 +312,35 @@ export const force_output_sequence = {
LAST: 2, LAST: 2,
}; };
/**
* Replaces instruct mode macros in the given sequence string.
* @param {string} sequence Sequence string.
* @param {string} name Item name.
* @param {import('moment').Moment} timestamp Message timestamp.
* @returns {string} Sequence string with macros replaced.
*/
function replaceSequenceMacros(sequence, name, timestamp) {
sequence = substituteParams(sequence);
sequence = sequence.replace(/{{name}}/gi, name || 'System');
sequence = sequence.replace(/{{timestamp}}/gi, () => timestamp.format('YYYY-MM-DD HH:mm:ss'));
sequence = sequence.replace(/{{timestamp::(.*?)}}/gi, (_, format) => timestamp.format(format));
return sequence;
}
/** /**
* Formats instruct mode chat message. * Formats instruct mode chat message.
* @param {string} name Character name. * @param {InstructFormatParams} params Message parameters.
* @param {string} mes Message text. * @typedef {object} InstructFormatParams Instruct mode chat message parameters.
* @param {boolean} isUser Is the message from the user. * @property {string} name Character name.
* @param {boolean} isNarrator Is the message from the narrator. * @property {string} mes Message text.
* @param {string} forceAvatar Force avatar string. * @property {boolean} isUser Is the message from the user.
* @param {string} name1 User name. * @property {boolean} isNarrator Is the message from the narrator.
* @param {string} name2 Character name. * @property {string} forceAvatar Force avatar string.
* @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured). * @property {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
* @property {import('moment').Moment} timestamp Message timestamp.
* @returns {string} Formatted instruct mode chat message. * @returns {string} Formatted instruct mode chat message.
*/ */
export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence) { export function formatInstructModeChat({ name, mes, isUser, isNarrator, forceAvatar, forceOutputSequence, timestamp }) {
let includeNames = isNarrator ? false : power_user.instruct.names_behavior === names_behavior_types.ALWAYS; let includeNames = isNarrator ? false : power_user.instruct.names_behavior === names_behavior_types.ALWAYS;
if (!isNarrator && power_user.instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) { if (!isNarrator && power_user.instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {
@@ -372,11 +391,8 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
let suffix = getSuffix() || ''; let suffix = getSuffix() || '';
if (power_user.instruct.macro) { if (power_user.instruct.macro) {
prefix = substituteParams(prefix, name1, name2); prefix = replaceSequenceMacros(prefix, (name || 'System'), timestamp);
prefix = prefix.replace(/{{name}}/gi, name || 'System'); suffix = replaceSequenceMacros(suffix, (name || 'System'), timestamp);
suffix = substituteParams(suffix, name1, name2);
suffix = suffix.replace(/{{name}}/gi, name || 'System');
} }
if (!suffix && power_user.instruct.wrap) { if (!suffix && power_user.instruct.wrap) {
@@ -420,11 +436,9 @@ export function formatInstructModeSystemPrompt(systemPrompt) {
/** /**
* Formats example messages according to instruct mode settings. * Formats example messages according to instruct mode settings.
* @param {string[]} mesExamplesArray Example messages array. * @param {string[]} mesExamplesArray Example messages array.
* @param {string} name1 User name.
* @param {string} name2 Character name.
* @returns {string[]} Formatted example messages string. * @returns {string[]} Formatted example messages string.
*/ */
export function formatInstructModeExamples(mesExamplesArray, name1, name2) { export function formatInstructModeExamples(mesExamplesArray) {
const blockHeading = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : ''; const blockHeading = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : '';
if (power_user.instruct.skip_examples) { if (power_user.instruct.skip_examples) {
@@ -440,15 +454,10 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
let outputSuffix = power_user.instruct.output_suffix || ''; let outputSuffix = power_user.instruct.output_suffix || '';
if (power_user.instruct.macro) { if (power_user.instruct.macro) {
inputPrefix = substituteParams(inputPrefix, name1, name2); inputPrefix = replaceSequenceMacros(inputPrefix, name1, moment());
outputPrefix = substituteParams(outputPrefix, name1, name2); outputPrefix = replaceSequenceMacros(outputPrefix, name2, moment());
inputSuffix = substituteParams(inputSuffix, name1, name2); inputSuffix = replaceSequenceMacros(inputSuffix, name1, moment());
outputSuffix = substituteParams(outputSuffix, name1, name2); outputSuffix = replaceSequenceMacros(outputSuffix, name2, moment());
inputPrefix = inputPrefix.replace(/{{name}}/gi, name1);
outputPrefix = outputPrefix.replace(/{{name}}/gi, name2);
inputSuffix = inputSuffix.replace(/{{name}}/gi, name1);
outputSuffix = outputSuffix.replace(/{{name}}/gi, name2);
if (!inputSuffix && power_user.instruct.wrap) { if (!inputSuffix && power_user.instruct.wrap) {
inputSuffix = '\n'; inputSuffix = '\n';
@@ -499,13 +508,11 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
* @param {string} name Character name. * @param {string} name Character name.
* @param {boolean} isImpersonate Is generation in impersonation mode. * @param {boolean} isImpersonate Is generation in impersonation mode.
* @param {string} promptBias Prompt bias string. * @param {string} promptBias Prompt bias string.
* @param {string} name1 User name.
* @param {string} name2 Character name.
* @param {boolean} isQuiet Is quiet mode generation. * @param {boolean} isQuiet Is quiet mode generation.
* @param {boolean} isQuietToLoud Is quiet to loud generation. * @param {boolean} isQuietToLoud Is quiet to loud generation.
* @returns {string} Formatted instruct mode last prompt line. * @returns {string} Formatted instruct mode last prompt line.
*/ */
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud) { export function formatInstructModePrompt(name, isImpersonate, promptBias, isQuiet, isQuietToLoud) {
const includeNames = name && (power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud); const includeNames = name && (power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud);
function getSequence() { function getSequence() {
@@ -545,8 +552,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
} }
if (power_user.instruct.macro) { if (power_user.instruct.macro) {
sequence = substituteParams(sequence, name1, name2); sequence = replaceSequenceMacros(sequence, (name || 'System'), moment());
sequence = sequence.replace(/{{name}}/gi, name || 'System');
} }
const separator = power_user.instruct.wrap ? '\n' : ''; const separator = power_user.instruct.wrap ? '\n' : '';

View File

@@ -3,17 +3,30 @@ import {
} from '../lib.js'; } from '../lib.js';
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { getCurrentLocale, t } from './i18n.js'; import { getCurrentLocale, t, translate } from './i18n.js';
import { MacrosParser } from './macros.js'; import { MacrosParser } from './macros.js';
import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js'; import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
import { Popup } from './popup.js'; import { Popup } from './popup.js';
import { power_user } from './power-user.js'; import { power_user } from './power-user.js';
import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js'; import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
/**
* Enum representing the type of the reasoning for a message (where it came from)
* @enum {string}
* @readonly
*/
export const ReasoningType = {
Model: 'model',
Parsed: 'parsed',
Manual: 'manual',
Edited: 'edited',
};
/** /**
* Gets a message from a jQuery element. * Gets a message from a jQuery element.
@@ -94,7 +107,7 @@ export function isHiddenReasoningModel() {
{ name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith }, { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
]; ];
const model = getChatCompletionModel(); const model = getChatCompletionModel() || '';
const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name)); const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
return isHidden; return isHidden;
@@ -129,7 +142,12 @@ export const ReasoningState = {
* This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates. * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
*/ */
export class ReasoningHandler { export class ReasoningHandler {
/** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
#isHiddenReasoningModel; #isHiddenReasoningModel;
/** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */
#isParsingReasoning = false;
/** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */
#parsingReasoningMesStartIndex = null;
/** /**
* @param {Date?} [timeStarted=null] - When the generation started * @param {Date?} [timeStarted=null] - When the generation started
@@ -137,6 +155,8 @@ export class ReasoningHandler {
constructor(timeStarted = null) { constructor(timeStarted = null) {
/** @type {ReasoningState} The current state of the reasoning process */ /** @type {ReasoningState} The current state of the reasoning process */
this.state = ReasoningState.None; this.state = ReasoningState.None;
/** @type {ReasoningType?} The type of the reasoning (where it came from) */
this.type = null;
/** @type {string} The reasoning output */ /** @type {string} The reasoning output */
this.reasoning = ''; this.reasoning = '';
/** @type {Date} When the reasoning started */ /** @type {Date} When the reasoning started */
@@ -147,7 +167,6 @@ export class ReasoningHandler {
/** @type {Date} Initial starting time of the generation */ /** @type {Date} Initial starting time of the generation */
this.initialTime = timeStarted ?? new Date(); this.initialTime = timeStarted ?? new Date();
/** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
this.#isHiddenReasoningModel = isHiddenReasoningModel(); this.#isHiddenReasoningModel = isHiddenReasoningModel();
// Cached DOM elements for reasoning // Cached DOM elements for reasoning
@@ -194,6 +213,7 @@ export class ReasoningHandler {
this.state = ReasoningState.Hidden; this.state = ReasoningState.Hidden;
} }
this.type = extra?.reasoning_type;
this.reasoning = extra?.reasoning ?? ''; this.reasoning = extra?.reasoning ?? '';
if (this.state !== ReasoningState.None) { if (this.state !== ReasoningState.None) {
@@ -208,6 +228,7 @@ export class ReasoningHandler {
// Make sure reset correctly clears all relevant states // Make sure reset correctly clears all relevant states
if (reset) { if (reset) {
this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None; this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
this.type = null;
this.reasoning = ''; this.reasoning = '';
this.initialTime = new Date(); this.initialTime = new Date();
this.startTime = null; this.startTime = null;
@@ -237,18 +258,19 @@ export class ReasoningHandler {
* Updates the reasoning text/string for a message. * Updates the reasoning text/string for a message.
* *
* @param {number} messageId - The ID of the message to update * @param {number} messageId - The ID of the message to update
* @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
* @param {Object} [options={}] - Optional arguments * @param {Object} [options={}] - Optional arguments
* @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
* @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one
* @returns {boolean} - Returns true if the reasoning was changed, otherwise false * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
*/ */
updateReasoning(messageId, reasoning = null, { persist = false } = {}) { updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
if (messageId == -1 || !chat[messageId]) { if (messageId == -1 || !chat[messageId]) {
return false; return false;
} }
reasoning = reasoning ?? this.reasoning; reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning; reasoning = trimSpaces(reasoning);
// Ensure the chat extra exists // Ensure the chat extra exists
if (!chat[messageId].extra) { if (!chat[messageId].extra) {
@@ -259,10 +281,13 @@ export class ReasoningHandler {
const reasoningChanged = extra.reasoning !== reasoning; const reasoningChanged = extra.reasoning !== reasoning;
this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING); this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
if (persist) { if (persist) {
// Build and save the reasoning data to message extras // Build and save the reasoning data to message extras
extra.reasoning = this.reasoning; extra.reasoning = this.reasoning;
extra.reasoning_duration = this.getDuration(); extra.reasoning_duration = this.getDuration();
extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
} }
return reasoningChanged; return reasoningChanged;
@@ -279,7 +304,10 @@ export class ReasoningHandler {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async process(messageId, mesChanged) { async process(messageId, mesChanged) {
if (!this.reasoning && !this.#isHiddenReasoningModel) return; mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged);
if (!this.reasoning && !this.#isHiddenReasoningModel)
return;
// Ensure reasoning string is updated and regexes are applied correctly // Ensure reasoning string is updated and regexes are applied correctly
const reasoningChanged = this.updateReasoning(messageId, null, { persist: true }); const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
@@ -294,6 +322,53 @@ export class ReasoningHandler {
} }
} }
#autoParseReasoningFromMessage(messageId, mesChanged) {
if (!power_user.reasoning.auto_parse)
return;
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix)
return mesChanged;
/** @type {{ mes: string, [key: string]: any}} */
const message = chat[messageId];
if (!message) return mesChanged;
// If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it.
if (this.#parsingReasoningMesStartIndex) {
message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex));
return mesChanged;
}
if (this.state === ReasoningState.None) {
// If streamed message starts with the opening, cut it out and put all inside reasoning
if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
this.#isParsingReasoning = true;
// Manually set starting state here, as we might already have received the ending suffix
this.state = ReasoningState.Thinking;
this.startTime = this.initialTime;
}
}
if (!this.#isParsingReasoning)
return mesChanged;
// If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block
const originalMes = message.mes;
this.reasoning = originalMes.slice(power_user.reasoning.prefix.length);
message.mes = '';
// If the reasoning contains the ending suffix, we cut that off and continue as message streaming
if (this.reasoning.includes(power_user.reasoning.suffix)) {
this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix));
this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length;
message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex));
this.#isParsingReasoning = false;
}
// Only return the original mesChanged value if we haven't cut off the complete message
return message.mes.length ? mesChanged : false;
}
/** /**
* Completes the reasoning process for a message. * Completes the reasoning process for a message.
* *
@@ -336,9 +411,10 @@ export class ReasoningHandler {
// Update states to the relevant DOM elements // Update states to the relevant DOM elements
setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null); setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state); setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
// Update the reasoning message // Update the reasoning message
const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning; const reasoning = trimSpaces(this.reasoning);
const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
this.messageReasoningContentDom.innerHTML = displayReasoning; this.messageReasoningContentDom.innerHTML = displayReasoning;
@@ -393,17 +469,14 @@ export class ReasoningHandler {
const element = this.messageReasoningHeaderDom; const element = this.messageReasoningHeaderDom;
const duration = this.getDuration(); const duration = this.getDuration();
let data = null; let data = null;
let title = '';
if (duration) { if (duration) {
const seconds = moment.duration(duration).asSeconds();
const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 }); const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
const secondsStr = moment.duration(duration).asSeconds(); element.textContent = t`Thought for ${durationStr}`;
data = String(seconds);
const span = document.createElement('span'); title = `${seconds} seconds`;
span.title = t`${secondsStr} seconds`;
span.textContent = durationStr;
element.textContent = t`Thought for `;
element.appendChild(span);
data = String(secondsStr);
} else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) { } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
element.textContent = t`Thought for some time`; element.textContent = t`Thought for some time`;
data = 'unknown'; data = 'unknown';
@@ -412,6 +485,12 @@ export class ReasoningHandler {
data = null; data = null;
} }
if (this.type !== ReasoningType.Model) {
title += ` [${translate(this.type)}]`;
title = title.trim();
}
element.title = title;
setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data); setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
setDatasetProperty(element, 'duration', data); setDatasetProperty(element, 'duration', data);
} }
@@ -573,11 +652,16 @@ function registerReasoningSlashCommands() {
callback: async (args, value) => { callback: async (args, value) => {
const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1; const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
const message = chat[messageId]; const message = chat[messageId];
if (!message?.extra) { if (!message) {
return ''; return '';
} }
// Make sure the message has an extra object
if (!message.extra || typeof message.extra !== 'object') {
message.extra = {};
}
message.extra.reasoning = String(value ?? ''); message.extra.reasoning = String(value ?? '');
message.extra.reasoning_type = ReasoningType.Manual;
await saveChatConditional(); await saveChatConditional();
closeMessageEditor('reasoning'); closeMessageEditor('reasoning');
@@ -598,7 +682,26 @@ function registerReasoningSlashCommands() {
typeList: [ARGUMENT_TYPE.BOOLEAN], typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true', defaultValue: 'true',
isRequired: false, isRequired: false,
enumProvider: commonEnumProviders.boolean('trueFalse'), enumList: commonEnumProviders.boolean('trueFalse')(),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'Whether to return the parsed reasoning or the content without reasoning',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'reasoning',
isRequired: false,
enumList: [
new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning),
new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message),
],
}),
SlashCommandNamedArgument.fromProps({
name: 'strict',
description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
isRequired: false,
enumList: commonEnumProviders.boolean('trueFalse')(),
}), }),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
@@ -608,19 +711,27 @@ function registerReasoningSlashCommands() {
}), }),
], ],
callback: (args, value) => { callback: (args, value) => {
if (!value) { if (!value || typeof value !== 'string') {
return ''; return '';
} }
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) { if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`); toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
return String(value); return value;
}
if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) {
toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`);
} }
const parsedReasoning = parseReasoningFromString(String(value)); const returnMessage = args.return === 'content';
const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
if (!parsedReasoning) { if (!parsedReasoning) {
return ''; return returnMessage ? value : '';
}
if (returnMessage) {
return parsedReasoning.content;
} }
const applyRegex = !isFalseBoolean(String(args.regex ?? '')); const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
@@ -720,6 +831,7 @@ function setReasoningEventHandlers() {
const textarea = messageBlock.find('.reasoning_edit_textarea'); const textarea = messageBlock.find('.reasoning_edit_textarea');
const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true }); const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
message.extra.reasoning = reasoning; message.extra.reasoning = reasoning;
message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
await saveChatConditional(); await saveChatConditional();
updateMessageBlock(messageId, message); updateMessageBlock(messageId, message);
textarea.remove(); textarea.remove();
@@ -780,6 +892,8 @@ function setReasoningEventHandlers() {
return; return;
} }
message.extra.reasoning = ''; message.extra.reasoning = '';
delete message.extra.reasoning_type;
delete message.extra.reasoning_duration;
await saveChatConditional(); await saveChatConditional();
updateMessageBlock(messageId, message); updateMessageBlock(messageId, message);
const textarea = messageBlock.find('.reasoning_edit_textarea'); const textarea = messageBlock.find('.reasoning_edit_textarea');
@@ -819,16 +933,18 @@ export function removeReasoningFromString(str) {
* @property {string} reasoning Reasoning block * @property {string} reasoning Reasoning block
* @property {string} content Message content * @property {string} content Message content
* @param {string} str Content of the message * @param {string} str Content of the message
* @param {Object} options Optional arguments
* @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it
* @returns {ParsedReasoning|null} Parsed reasoning block and message content * @returns {ParsedReasoning|null} Parsed reasoning block and message content
*/ */
function parseReasoningFromString(str) { function parseReasoningFromString(str, { strict = true } = {}) {
// Both prefix and suffix must be defined // Both prefix and suffix must be defined
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) { if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
return null; return null;
} }
try { try {
const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's'); const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
let didReplace = false; let didReplace = false;
let reasoning = ''; let reasoning = '';
@@ -838,9 +954,9 @@ function parseReasoningFromString(str) {
return ''; return '';
}); });
if (didReplace && power_user.trim_spaces) { if (didReplace) {
reasoning = reasoning.trim(); reasoning = trimSpaces(reasoning);
content = content.trim(); content = trimSpaces(content);
} }
return { reasoning, content }; return { reasoning, content };
@@ -869,6 +985,11 @@ function registerReasoningAppEvents() {
return null; return null;
} }
if (message.extra?.reasoning) {
console.debug('[Reasoning] Message already has reasoning', idx);
return null;
}
const parsedReasoning = parseReasoningFromString(message.mes); const parsedReasoning = parseReasoningFromString(message.mes);
// No reasoning block found // No reasoning block found
@@ -886,6 +1007,7 @@ function registerReasoningAppEvents() {
// If reasoning was found, add it to the message // If reasoning was found, add it to the message
if (parsedReasoning.reasoning) { if (parsedReasoning.reasoning) {
message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING); message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
message.extra.reasoning_type = ReasoningType.Parsed;
} }
// Update the message text if it was changed // Update the message text if it was changed

View File

@@ -34,6 +34,7 @@ export const enumIcons = {
preset: '⚙️', preset: '⚙️',
file: '📄', file: '📄',
message: '💬', message: '💬',
reasoning: '💡',
voice: '🎤', voice: '🎤',
server: '🖥️', server: '🖥️',
popup: '🗔', popup: '🗔',

View File

@@ -311,7 +311,7 @@ export function validateTextGenUrl() {
const formattedUrl = formatTextGenURL(url); const formattedUrl = formatTextGenURL(url);
if (!formattedUrl) { if (!formattedUrl) {
toastr.error('Enter a valid API URL', 'Text Completion API'); toastr.error(t`Enter a valid API URL`, 'Text Completion API');
return; return;
} }
@@ -1187,7 +1187,7 @@ export function getTextGenModel() {
return settings.aphrodite_model; return settings.aphrodite_model;
case OLLAMA: case OLLAMA:
if (!settings.ollama_model) { if (!settings.ollama_model) {
toastr.error('No Ollama model selected.', 'Text Completion API'); toastr.error(t`No Ollama model selected.`, 'Text Completion API');
throw new Error('No Ollama model selected'); throw new Error('No Ollama model selected');
} }
return settings.ollama_model; return settings.ollama_model;

View File

@@ -8,7 +8,7 @@ import {
import { getContext } from './extensions.js'; import { getContext } from './extensions.js';
import { characters, getRequestHeaders, this_chid } from '../script.js'; import { characters, getRequestHeaders, this_chid } from '../script.js';
import { isMobile } from './RossAscends-mods.js'; import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js'; import { collapseNewlines, power_user } from './power-user.js';
import { debounce_timeout } from './constants.js'; import { debounce_timeout } from './constants.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -676,6 +676,19 @@ export function sortByCssOrder(a, b) {
return _a - _b; return _a - _b;
} }
/**
* Trims leading and trailing whitespace from the input string based on a configuration setting.
* @param {string} input - The string to be trimmed
* @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string
*/
export function trimSpaces(input) {
if (!input || typeof input !== 'string') {
return input;
}
return power_user.trim_spaces ? input.trim() : input;
}
/** /**
* Trims a string to the end of a nearest sentence. * Trims a string to the end of a nearest sentence.
* @param {string} input The string to trim. * @param {string} input The string to trim.
@@ -892,6 +905,11 @@ function parseTimestamp(timestamp) {
return new Date(unixTime).toISOString(); return new Date(unixTime).toISOString();
} }
// ISO 8601
if (moment(timestamp, moment.ISO_8601, true).isValid()) {
return timestamp;
}
let dtFmt = []; let dtFmt = [];
// meridiem-based format // meridiem-based format
@@ -918,6 +936,7 @@ function parseTimestamp(timestamp) {
if (!rgxMatch) continue; if (!rgxMatch) continue;
return x.callback(...rgxMatch); return x.callback(...rgxMatch);
} }
return; return;
} }
@@ -994,13 +1013,18 @@ export function getImageSizeFromDataURL(dataUrl) {
}); });
} }
export function getCharaFilename(chid) { /**
* Gets the filename of the character avatar without extension
* @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID
* @param {object} [options={}] - Options arguments
* @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
* @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid
*/
export function getCharaFilename(chid = null, { manualAvatarKey = null } = {}) {
const context = getContext(); const context = getContext();
const fileName = context.characters[chid ?? context.characterId]?.avatar; const fileName = manualAvatarKey ?? context.characters[chid ?? context.characterId]?.avatar;
if (fileName) { return fileName?.replace(/\.[^/.]+$/, '') ?? null;
return fileName.replace(/\.[^/.]+$/, '');
}
} }
/** /**

View File

@@ -438,7 +438,7 @@ input[type='checkbox']:focus-visible {
} }
/** If hidden reasoning should not be shown, we hide all blocks that don't have content */ /** If hidden reasoning should not be shown, we hide all blocks that don't have content */
#chat:not([data-show-hidden-reasoning="true"]) .mes:has(.mes_reasoning:empty) .mes_reasoning_details { #chat:not([data-show-hidden-reasoning="true"]):not(:has(.reasoning_edit_textarea)) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
display: none; display: none;
} }

View File

@@ -132,35 +132,35 @@ function getSourceSettings(source, request) {
switch (source) { switch (source) {
case 'togetherai': case 'togetherai':
return { return {
model: String(request.headers['x-togetherai-model']), model: String(request.body.model),
}; };
case 'openai': case 'openai':
return { return {
model: String(request.headers['x-openai-model']), model: String(request.body.model),
}; };
case 'cohere': case 'cohere':
return { return {
model: String(request.headers['x-cohere-model']), model: String(request.body.model),
}; };
case 'llamacpp': case 'llamacpp':
return { return {
apiUrl: String(request.headers['x-llamacpp-url']), apiUrl: String(request.body.apiUrl),
}; };
case 'vllm': case 'vllm':
return { return {
apiUrl: String(request.headers['x-vllm-url']), apiUrl: String(request.body.apiUrl),
model: String(request.headers['x-vllm-model']), model: String(request.body.model),
}; };
case 'ollama': case 'ollama':
return { return {
apiUrl: String(request.headers['x-ollama-url']), apiUrl: String(request.body.apiUrl),
model: String(request.headers['x-ollama-model']), model: String(request.body.model),
keep: Boolean(request.headers['x-ollama-keep']), keep: Boolean(request.body.keep),
}; };
case 'extras': case 'extras':
return { return {
extrasUrl: String(request.headers['x-extras-url']), extrasUrl: String(request.body.extrasUrl),
extrasKey: String(request.headers['x-extras-key']), extrasKey: String(request.body.extrasKey),
}; };
case 'transformers': case 'transformers':
return { return {