Merge branch 'staging' into fix-char-rename-aux-connections

434bdab585beaeb8e051cac9997c6242da0b6774

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

20 files changed, +368 -182Showing whitespace changes
package-lock.json+0 -12
@@ -87,7 +87,6 @@
8787 "@types/cookie-session": "^2.0.49",
8888 "@types/cors": "^2.8.17",
8989 "@types/deno": "^2.0.0",
90- "@types/dompurify": "^3.2.0",
9190 "@types/express": "^4.17.21",
9291 "@types/jquery": "^3.5.29",
9392 "@types/jquery-cropper": "^1.0.4",
@@ -1181,17 +1180,6 @@
11811180 "dev": true,
11821181 "license": "MIT"
11831182 },
1184- "node_modules/@types/dompurify": {
1185- "version": "3.2.0",
1186- "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz",
1187- "integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==",
1188- "deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.",
1189- "dev": true,
1190- "license": "MIT",
1191- "dependencies": {
1192- "dompurify": "*"
1193- }
1194- },
11951183 "node_modules/@types/estree": {
11961184 "version": "1.0.6",
11971185 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
package.json+0 -1
@@ -116,7 +116,6 @@
116116 "@types/cookie-session": "^2.0.49",
117117 "@types/cors": "^2.8.17",
118118 "@types/deno": "^2.0.0",
119- "@types/dompurify": "^3.2.0",
120119 "@types/express": "^4.17.21",
121120 "@types/jquery": "^3.5.29",
122121 "@types/jquery-cropper": "^1.0.4",
public/index.html+4 -4
@@ -2015,7 +2015,7 @@
20152015 </div>
20162016 </div>
20172017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
20182018 <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.">
20192019 <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
20202020 Reasoning Effort
20212021 </label>
@@ -4001,7 +4001,7 @@
40014001 <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">
40024002 <small>
40034003 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
40044004 <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>
40054005 </small>
40064006 <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">
40074007 <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 @@
68906890 </div>
68916891 <div id="form_sheld">
68926892 <div id="dialogue_del_mes">
68936893 <div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div>
68946894 <div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div>
68956895 </div>
68966896 <div id="send_form" class="no-connection">
68976897 <form id="file_form" class="wide100p displayNone">
public/locales/fr-fr.json+1 -1
@@ -1485,7 +1485,7 @@
14851485 "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",
14861486 "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",
14871487 "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",
14881488 "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)",
14891489 "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",
14901490 "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",
14911491 "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",
public/locales/ru-ru.json+72 -12
@@ -195,7 +195,7 @@
195195 "Yes": "Да",
196196 "No": "Нет",
197197 "Context %": "Процент контекста",
198198 "Budget Cap": "БюджетныйЛимит лимитбюджета",
199199 "(0 = disabled)": "(0 = отключено)",
200200 "None": "Отсутствует",
201201 "User Settings": "Настройки пользователя",
@@ -575,10 +575,10 @@
575575 "Characters sorting order": "Порядок сортировки персонажей",
576576 "Remove": "Убрать",
577577 "Select a World Info file for": "Выбрать файл с миром для",
578578 "Primary Lorebook": "ОсновногоОсновной лорбукалорбук",
579579 "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.",
580580 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.",
581581 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.",
582582 "Additional Lorebooks": "Вспомогательные лорбуки",
583583 "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",
584584 "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",
@@ -593,7 +593,7 @@
593593 "Prompt": "Промпт",
594594 "Copy": "Скопировать",
595595 "Confirm": "Подтвердить",
596596 "Copy this message": "СкопироватьПродублировать сообщение",
597597 "Delete this message": "Удалить сообщение",
598598 "Move message up": "Переместить сообщение вверх",
599599 "Move message down": "Переместить сообщение вниз",
@@ -612,7 +612,7 @@
612612 "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",
613613 "Continue the last message": "Продолжить текущее сообщение",
614614 "Bind user name to that avatar": "Закрепить имя за этим аватаром",
615615 "Select this as default persona for the new chats.": "ВыбератьВыбирать эту Персону в качестве персоныперсону по умолчанию для всех новых чатов.",
616616 "Change persona image": "Сменить аватар персоны",
617617 "Delete persona": "Удалить персону",
618618 "Reduced Motion": "Сокращение анимаций",
@@ -640,7 +640,7 @@
640640 "Token Probabilities": "Вероятности токенов",
641641 "Close chat": "Закрыть чат",
642642 "Manage chat files": "Все чаты",
643643 "Import Extension From Git Repo": "Импортировать расширение из Git Repository-репозитория.",
644644 "Install extension": "Установить расширение",
645645 "Manage extensions": "Управление расширениями",
646646 "Tokens persona description": "Токенов",
@@ -1122,7 +1122,7 @@
11221122 "help_hotkeys_0": "Горячие клавиши",
11231123 "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",
11241124 "Download Extensions & Assets": "Загрузить расширения и ресурсы",
11251125 "menu within": "внутри этихв кубиковменю",
11261126 "Assets URL": "URL с описанием ресурсов",
11271127 "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",
11281128 "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",
@@ -1943,7 +1943,7 @@
19431943 "and connect to an": "и подключитесь к",
19441944 "You can add more": "Можете добавить больше",
19451945 "from other websites": "с других сайтов.",
19461946 "Go to the": "ЗаглянитеЗаходите в",
19471947 "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
19481948 "or_welcome": "; также доступен",
19491949 "Claude API Key": "Ключ от API Claude",
@@ -1958,7 +1958,7 @@
19581958 "Save": "Сохранить",
19591959 "Chat Lorebook": "Лорбук для чата",
19601960 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",
19611961 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + кликЛКМ, чтобы открыть диалог привязки мира",
19621962 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
19631963 "ext_regex_user_input_desc": "Отправленные вами сообщения.",
19641964 "ext_regex_ai_input_desc": "Полученные от API ответы.",
@@ -2144,5 +2144,65 @@
21442144 "Not connected to the API!": "Нет соединения с API!",
21452145 "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
21462146 "Update all": "Обновить все",
21472147 "Close": "Закрыть",
2148+ "Optional modules:": "Необязательные модули:",
2149+ "Sort: Display Name": "Сортировать: по названию",
2150+ "Sort: Loading Order": "Сортировать: в порядке загрузки",
2151+ "Click to toggle": "Нажмите, чтобы включить или выключить",
2152+ "Loading Asset List": "Загрузить список ресурсов",
2153+ "Don't ask again for this URL": "Запомнить выбор для этого адреса",
2154+ "Are you sure you want to connect to the following url?": "Вы точно хотите подключиться к этому адресу?",
2155+ "All": "Всё",
2156+ "Characters": "Персонажи",
2157+ "Ambient sounds": "Звуковой эмбиент",
2158+ "Blip sounds": "Звуки уведомлений",
2159+ "Background music": "Фоновая музыка",
2160+ "Search": "Поиск",
2161+ "extension_install_1": "Чтобы загружать расширения из этого списка, у вас должен быть установлен ",
2162+ "extension_install_2": ".",
2163+ "extension_install_3": "Нажмите на иконку ",
2164+ "extension_install_4": ", чтобы перейти в репозиторий расширения и получить более подробную информацию о нём.",
2165+ "Extension repo/guide:": "Репозиторий расширения:",
2166+ "Preview in browser": "Предпросмотр",
2167+ "Adds a function tool": "Частично или полностью работает через вызов функций",
2168+ "Tool": "Функции",
2169+ "Move extension": "Переместить расширение",
2170+ "ext_type_local": "Это локальное расширение, доступно только вам",
2171+ "ext_type_global": "Это глобальное расширение, доступно всем пользователям",
2172+ "Move": "Переместить",
2173+ "Enter the Git URL of the extension to install": "Введите Git-адрес расширения",
2174+ "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.": "помните, что используя расширения от сторонних авторов, вы можете подвергать систему опасности. Устанавливайте расширения только от проверенных разработчиков. Мы не несём ответственности за любой ущерб, причинённый сторонними расширениями.",
2175+ "Disclaimer:": "Внимание:",
2176+ "Example:": "Пример:",
2177+ "context_derived": "Считывать из метаданных модели (по возможности)",
2178+ "instruct_derived": "Считывать из метаданных модели (по возможности)",
2179+ "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте",
2180+ "Reasoning Effort": "Рассуждения",
2181+ "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.",
2182+ "openai_reasoning_effort_low": "Поверхностные",
2183+ "openai_reasoning_effort_medium": "Обычные",
2184+ "openai_reasoning_effort_high": "Подробные",
2185+ "Persona Lore Alt+Click to open the lorebook": "Лорбук данной персоны\nAlt + ЛКМ чтобы открыть лорбук",
2186+ "Persona Lorebook for": "Лорбук для персоны",
2187+ "persona_world_template_txt": "Выбранная Информация о мире будет привязана к этой персоне. Информация будет добавляться в каждом промпте вместе с глобальным лорбуком и лорбуками персонажа и чата.",
2188+ "Global list": "Глобальный список",
2189+ "Preset-specific list": "Список для данного пресета",
2190+ "Banned tokens/strings are being sent in the request.": "Запрещённые токены и строки отсылаются в запросе.",
2191+ "Banned tokens/strings are NOT being sent in the request.": "Запрещённые токены и строки НЕ отсылаются в запросе.",
2192+ "Add a reasoning block": "Добавить блок рассуждений",
2193+ "Create a copy of this message?": "Продублировать это сообщение?",
2194+ "Max Recursion Steps": "Макс. глубина рекурсии",
2195+ "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = неограничено, 1 = сканировать единожды, 2 = сканировать единожды и сделать один повторный проход, и т.д.\n(неактивно при указанном мин. числе активаций)",
2196+ "(disabled when max recursion steps are used)": "(неактивно при указанной макс. глубине рекурсии)",
2197+ "Enter a valid API URL": "Введите корректный адрес API",
2198+ "No Ollama model selected.": "Не выбрана модель Ollama",
2199+ "Background Fitting": "Способ подгонки фона под разрешение",
2200+ "Chat Lore Alt+Click to open the lorebook": "Лорбук данного чата\nAlt + ЛКМ чтобы открыть лорбук",
2201+ "Token Counter": "Подсчитать токены",
2202+ "Type / paste in the box below to see the number of tokens in the text.": "Введите или вставьте текст в окошко ниже, чтобы подсчитать количество токенов в нём.",
2203+ "Selected tokenizer:": "Выбранный токенайзер:",
2204+ "Input:": "Входные данные:",
2205+ "Tokenized text:": "Токенизированный текст:",
2206+ "Token IDs:": "Идентификаторы токенов:",
2207+ "Tokens:": "Токенов:"
21482208}
21482208 \ No newline at end of file
public/locales/zh-cn.json+1 -1
@@ -584,7 +584,7 @@
584584 "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)",
585585 "Cap the number of entry activation recursions": "限制条目激活递归的次数",
586586 "Max Recursion Steps": "最大递归深度",
587587 "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(当使用最小激活次数时,此功能被禁用)",
588588 "Insertion Strategy": "插入策略",
589589 "Sorted Evenly": "均匀排序",
590590 "Character Lore First": "角色世界书优先",
public/locales/zh-tw.json+2 -2
@@ -1458,7 +1458,7 @@
14581458 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",
14591459 "popup-button-crop": "裁剪",
14601460 "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
14611461 "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(使用最小啟動設定時將停用)",
14621462 "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)。",
14631463 "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",
14641464 "Abort current image generation task": "終止目前的圖片生成任務",
@@ -1806,7 +1806,7 @@
18061806 "context_derived": "若可能,根據模型元數據推導。",
18071807 "instruct_derived": "若可能,根據模型元數據推導。",
18081808 "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",
18091809 "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(啟用最小啟動次數時無效)",
18101810 "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",
18111811 "Manual": "手動",
18121812 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 格式:specpost_history_instructions)",
public/script.js+7 -2
@@ -366,6 +366,10 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
366366 return;
367367 }
368368
369+ if (!(node instanceof Element)) {
370+ return;
371+ }
372+
369373 let mediaBlocked = false;
370374
371375 switch (node.tagName) {
@@ -3234,6 +3238,7 @@ class StreamingProcessor {
32343238
32353239 // Update reasoning
32363240 await this.reasoningHandler.process(messageId, mesChanged);
3241+ processedText = chat[messageId]['mes'];
32373242
32383243 // Token count update.
32393244 const tokenCountText = this.reasoningHandler.reasoning + processedText;
@@ -3384,7 +3389,7 @@ class StreamingProcessor {
33843389 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33853390 }
33863391 // Get the updated reasoning string into the handler
33873392 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');
33883393 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33893394 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
33903395 }
@@ -10984,7 +10989,7 @@ jQuery(async function () {
1098410989 });
1098510990
1098610991 $(document).on('click', '.mes_edit_copy', async function () {
1098710992 const confirmation = await callGenericPopup('t`Create a copy of this message?'`, POPUP_TYPE.CONFIRM);
1098810993 if (!confirmation) {
1098910994 return;
1099010995 }
public/scripts/extensions.js+4 -4
@@ -603,12 +603,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
603603 }
604604
605605 let toggleElement = isActive || isDisabled ?
606606 `'<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
607607 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
608608
609609 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>` : '';
610610 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>` : '';
611611 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>` : '';
612612 let modulesInfo = '';
613613
614614 if (isActive && Array.isArray(manifest.optional)) {
@@ -616,7 +616,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
616616 modules.forEach(x => optional.delete(x));
617617 if (optional.size > 0) {
618618 const optionalString = DOMPurify.sanitize([...optional].join(', '));
619619 modulesInfo = `'<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
620620 }
621621 } else if (!isDisabled) { // Neither active nor disabled
622622 const requirements = new Set(manifest.requires);
public/scripts/extensions/assets/index.js+13 -15
@@ -10,6 +10,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
1010import { executeSlashCommands } from '../../slash-commands.js';
1111import { accountStorage } from '../../util/AccountStorage.js';
1212import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
13+import { t } from '../../i18n.js';
1314export { MODULE_NAME };
1415
1516const MODULE_NAME = 'assets';
@@ -59,11 +60,11 @@ const KNOWN_TYPES = {
5960 'blip': 'Blip sounds',
6061};
6162
6263async function downloadAssetsList(url) {
6364 updateCurrentAssets().then(async function () {
6465 fetch(url, { cache: 'no-cache' })
6566 .then(response => response.json())
6667 .then(jsonasync =>function(json) {
6768
6869 availableAssets = {};
6970 $('#assets_menu').empty();
@@ -84,10 +85,10 @@ function downloadAssetsList(url) {
8485
8586 $('#assets_type_select').empty();
8687 $('#assets_search').val('');
8788 $('#assets_type_select').append($('<option />', { value: '', text: 't`All'` }));
8889
8990 for (const type of assetTypes) {
9091 const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
9192 $('#assets_type_select').append(option);
9293 }
9394
@@ -104,11 +105,7 @@ function downloadAssetsList(url) {
104105 assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
105106
106107 if (assetType == 'extension') {
107- assetTypeMenu.append(`
108+ assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
108- <div class="assets-list-git">
109- To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
110- 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.
111- </div>`);
112109 }
113110
114111 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) {
184181 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
185182 const description = DOMPurify.sanitize(asset['description'] || '');
186183 const url = isValidUrl(asset['url']) ? asset['url'] : '';
187184 const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : 't`Preview in browser'`;
188185 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
189186 const toolTag = assetType === 'extension' && asset['tool'];
190187
@@ -195,9 +192,10 @@ function downloadAssetsList(url) {
195192 <b>${displayName}</b>
196193 <a class="asset_preview" href="${url}" target="_blank" title="${title}">
197194 <i class="fa-solid fa-sm ${previewIcon}"></i>
198195 </a>` +
199196 ${(toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}+
200- </span>
197+ t`Tool` + '</span>' : '') +
198+ `</span>
201199 <small class="asset-description">
202200 ${description}
203201 </small>
@@ -435,7 +433,7 @@ jQuery(async () => {
435433 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
436434 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
437435
438436 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>`, {
439437 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
440438 onClose: popup => {
441439 if (popup.result) {
public/scripts/extensions/assets/installation.html+4 -0
@@ -0,0 +1,4 @@
1+<div class="assets-list-git">
2+ <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>
3+ <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>
4+</div>
4 \ No newline at end of file
public/scripts/extensions/assets/window.html+1 -1
@@ -33,7 +33,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
3333 <div id="assets_filters" class="flex-container">
3434 <select id="assets_type_select" class="text_pole flex1">
3535 </select>
3636 <input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
3737 <div id="assets-characters-button" class="menu_button menu_button_icon">
3838 <i class="fa-solid fa-image-portrait"></i>
3939 <span data-i18n="Characters">Characters</span>
public/scripts/extensions/token-counter/index.js+6 -20
@@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
66import { resetScrollHeight, debounce } from '../../utils.js';
77import { debounce_timeout } from '../../constants.js';
88import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
9+import { renderExtensionTemplateAsync } from '../../extensions.js';
10+import { t } from '../../i18n.js';
911
1012function rgb2hex(rgb) {
1113 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@@ -22,23 +24,7 @@ $('button').click(function () {
2224
2325async function doTokenCounter() {
2426 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
25- const html = `
27+ const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
26- <div class="wide100p">
27- <h3>Token Counter</h3>
28- <div class="justifyLeft flex-container flexFlowColumn">
29- <h4>Type / paste in the box below to see the number of tokens in the text.</h4>
30- <p>Selected tokenizer: ${tokenizerName}</p>
31- <div>Input:</div>
32- <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
33- <div>Tokens: <span id="token_counter_result">0</span></div>
34- <hr>
35- <div>Tokenized text:</div>
36- <div id="tokenized_chunks_display" class="wide100p">—</div>
37- <hr>
38- <div>Token IDs:</div>
39- <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
40- </div>
41- </div>`;
4228
4329 const dialog = $(html);
4430 const countDebounced = debounce(async () => {
@@ -131,9 +117,9 @@ async function doCount() {
131117jQuery(() => {
132118 const buttonHtml = `
133119 <div id="token_counter" class="list-group-item flex-container flexGap5">
134120 <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
135121 t`Token Counter` +
136122 '</div>`';
137123 $('#token_counter_wand_container').append(buttonHtml);
138124 $('#token_counter').on('click', doTokenCounter);
139125 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
public/scripts/extensions/token-counter/window.html+16 -0
@@ -0,0 +1,16 @@
1+<div class="wide100p">
2+ <h3 data-i18n="Token Counter">Token Counter</h3>
3+ <div class="justifyLeft flex-container flexFlowColumn">
4+ <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>
5+ <p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p>
6+ <div data-i18n="Input:">Input:</div>
7+ <textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
8+ <div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div>
9+ <hr>
10+ <div data-i18n="Tokenized text:">Tokenized text:</div>
11+ <div id="tokenized_chunks_display" class="wide100p">—</div>
12+ <hr>
13+ <div data-i18n="Token IDs:">Token IDs:</div>
14+ <textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
15+ </div>
16+</div>
16 \ No newline at end of file
public/scripts/extensions/vectors/index.js+56 -62
@@ -746,74 +746,65 @@ async function getQueryText(chat, initiator) {
746746}
747747
748748/**
749749 * Gets thecommon savedbody hashesparameters for avector collectionrequests.
750750 * @paramreturns {stringobject} collectionId
751-* @returns {Promise<number[]>} Saved hashes
752751 */
753752async function getSavedHashesgetVectorsRequestBody(collectionId) {
754- const response = await fetch('/api/vector/list', {
753+ const body = {};
755- method: 'POST',
756- headers: getVectorHeaders(),
757- body: JSON.stringify({
758- collectionId: collectionId,
759- source: settings.source,
760- }),
761- });
762-
763- if (!response.ok) {
764- throw new Error(`Failed to get saved hashes for collection ${collectionId}`);
765- }
766-
767- const hashes = await response.json();
768- return hashes;
769-}
770-
771-function getVectorHeaders() {
772- const headers = getRequestHeaders();
773754 switch (settings.source) {
774755 case 'extras':
775- Object.assign(headers, {
756+ body.extrasUrl = extension_settings.apiUrl;
776- 'X-Extras-Url': extension_settings.apiUrl,
757+ body.extrasKey = extension_settings.apiKey;
777- 'X-Extras-Key': extension_settings.apiKey,
778- });
779758 break;
780759 case 'togetherai':
781- Object.assign(headers, {
760+ body.model = extension_settings.vectors.togetherai_model;
782- 'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
783- });
784761 break;
785762 case 'openai':
786- Object.assign(headers, {
763+ body.model = extension_settings.vectors.openai_model;
787- 'X-OpenAI-Model': extension_settings.vectors.openai_model,
788- });
789764 break;
790765 case 'cohere':
791- Object.assign(headers, {
766+ body.model = extension_settings.vectors.cohere_model;
792- 'X-Cohere-Model': extension_settings.vectors.cohere_model,
793- });
794767 break;
795768 case 'ollama':
796- Object.assign(headers, {
769+ body.model = extension_settings.vectors.ollama_model;
797- 'X-Ollama-Model': extension_settings.vectors.ollama_model,
770+ body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
798- 'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],
771+ body.keep = !!extension_settings.vectors.ollama_keep;
799- 'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
800- });
801772 break;
802773 case 'llamacpp':
803- Object.assign(headers, {
774+ body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
804- 'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
805- });
806775 break;
807776 case 'vllm':
808- Object.assign(headers, {
777+ body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
809- 'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
778+ body.model = extension_settings.vectors.vllm_model;
810- 'X-Vllm-Model': extension_settings.vectors.vllm_model,
811- });
812779 break;
813780 default:
814781 break;
815782 }
816783 return headersbody;
784+}
785+
786+/**
787+ * Gets the saved hashes for a collection
788+* @param {string} collectionId
789+* @returns {Promise<number[]>} Saved hashes
790+*/
791+async function getSavedHashes(collectionId) {
792+ const response = await fetch('/api/vector/list', {
793+ method: 'POST',
794+ headers: getRequestHeaders(),
795+ body: JSON.stringify({
796+ ...getVectorsRequestBody(),
797+ collectionId: collectionId,
798+ source: settings.source,
799+ }),
800+ });
801+
802+ if (!response.ok) {
803+ throw new Error(`Failed to get saved hashes for collection ${collectionId}`);
804+ }
805+
806+ const hashes = await response.json();
807+ return hashes;
817808}
818809
819810/**
@@ -825,12 +816,11 @@ function getVectorHeaders() {
825816async function insertVectorItems(collectionId, items) {
826817 throwIfSourceInvalid();
827818
828- const headers = getVectorHeaders();
829-
830819 const response = await fetch('/api/vector/insert', {
831820 method: 'POST',
832821 headers: headersgetRequestHeaders(),
833822 body: JSON.stringify({
823+ ...getVectorsRequestBody(),
834824 collectionId: collectionId,
835825 items: items,
836826 source: settings.source,
@@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
879869async function deleteVectorItems(collectionId, hashes) {
880870 const response = await fetch('/api/vector/delete', {
881871 method: 'POST',
882872 headers: getVectorHeadersgetRequestHeaders(),
883873 body: JSON.stringify({
874+ ...getVectorsRequestBody(),
884875 collectionId: collectionId,
885876 hashes: hashes,
886877 source: settings.source,
@@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
899890 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
900891 */
901892async function queryCollection(collectionId, searchText, topK) {
902- const headers = getVectorHeaders();
903-
904893 const response = await fetch('/api/vector/query', {
905894 method: 'POST',
906895 headers: headersgetRequestHeaders(),
907896 body: JSON.stringify({
897+ ...getVectorsRequestBody(),
908898 collectionId: collectionId,
909899 searchText: searchText,
910900 topK: topK,
@@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
929919 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
930920 */
931921async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
932- const headers = getVectorHeaders();
933-
934922 const response = await fetch('/api/vector/query-multi', {
935923 method: 'POST',
936924 headers: headersgetRequestHeaders(),
937925 body: JSON.stringify({
926+ ...getVectorsRequestBody(),
938927 collectionIds: collectionIds,
939928 searchText: searchText,
940929 topK: topK,
@@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
965954
966955 const response = await fetch('/api/vector/purge', {
967956 method: 'POST',
968957 headers: getVectorHeadersgetRequestHeaders(),
969958 body: JSON.stringify({
959+ ...getVectorsRequestBody(),
970960 collectionId: collectionId,
971961 }),
972962 });
@@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
994984
995985 const response = await fetch('/api/vector/purge', {
996986 method: 'POST',
997987 headers: getVectorHeadersgetRequestHeaders(),
998988 body: JSON.stringify({
989+ ...getVectorsRequestBody(),
999990 collectionId: collectionId,
1000991 }),
1001992 });
@@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
10191010 try {
10201011 const response = await fetch('/api/vector/purge-all', {
10211012 method: 'POST',
10221013 headers: getVectorHeadersgetRequestHeaders(),
1014+ body: JSON.stringify({
1015+ ...getVectorsRequestBody(),
1016+ }),
10231017 });
10241018
10251019 if (!response.ok) {
public/scripts/reasoning.js+153 -31
@@ -3,17 +3,30 @@ import {
33} from '../lib.js';
44import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
55import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
66import { getCurrentLocale, t, translate } from './i18n.js';
77import { MacrosParser } from './macros.js';
88import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
99import { Popup } from './popup.js';
1010import { power_user } from './power-user.js';
1111import { SlashCommand } from './slash-commands/SlashCommand.js';
1212import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
1313import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14+import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
1415import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1516import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
1617import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
18+
19+/**
20+ * Enum representing the type of the reasoning for a message (where it came from)
21+ * @enum {string}
22+ * @readonly
23+ */
24+export const ReasoningType = {
25+ Model: 'model',
26+ Parsed: 'parsed',
27+ Manual: 'manual',
28+ Edited: 'edited',
29+};
1730
1831/**
1932 * Gets a message from a jQuery element.
@@ -129,7 +142,12 @@ export const ReasoningState = {
129142 * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
130143 */
131144export class ReasoningHandler {
145+ /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
132146 #isHiddenReasoningModel;
147+ /** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */
148+ #isParsingReasoning = false;
149+ /** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */
150+ #parsingReasoningMesStartIndex = null;
133151
134152 /**
135153 * @param {Date?} [timeStarted=null] - When the generation started
@@ -137,6 +155,8 @@ export class ReasoningHandler {
137155 constructor(timeStarted = null) {
138156 /** @type {ReasoningState} The current state of the reasoning process */
139157 this.state = ReasoningState.None;
158+ /** @type {ReasoningType?} The type of the reasoning (where it came from) */
159+ this.type = null;
140160 /** @type {string} The reasoning output */
141161 this.reasoning = '';
142162 /** @type {Date} When the reasoning started */
@@ -147,7 +167,6 @@ export class ReasoningHandler {
147167 /** @type {Date} Initial starting time of the generation */
148168 this.initialTime = timeStarted ?? new Date();
149169
150- /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
151170 this.#isHiddenReasoningModel = isHiddenReasoningModel();
152171
153172 // Cached DOM elements for reasoning
@@ -194,6 +213,7 @@ export class ReasoningHandler {
194213 this.state = ReasoningState.Hidden;
195214 }
196215
216+ this.type = extra?.reasoning_type;
197217 this.reasoning = extra?.reasoning ?? '';
198218
199219 if (this.state !== ReasoningState.None) {
@@ -208,6 +228,7 @@ export class ReasoningHandler {
208228 // Make sure reset correctly clears all relevant states
209229 if (reset) {
210230 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
231+ this.type = null;
211232 this.reasoning = '';
212233 this.initialTime = new Date();
213234 this.startTime = null;
@@ -237,18 +258,19 @@ export class ReasoningHandler {
237258 * Updates the reasoning text/string for a message.
238259 *
239260 * @param {number} messageId - The ID of the message to update
240261 * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
241262 * @param {Object} [options={}] - Optional arguments
242263 * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
264+ * @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one
243265 * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
244266 */
245267 updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
246268 if (messageId == -1 || !chat[messageId]) {
247269 return false;
248270 }
249271
250272 reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
251273 reasoning = power_user.trim_spaces ? reasoning.trimtrimSpaces() : reasoning);
252274
253275 // Ensure the chat extra exists
254276 if (!chat[messageId].extra) {
@@ -259,10 +281,13 @@ export class ReasoningHandler {
259281 const reasoningChanged = extra.reasoning !== reasoning;
260282 this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
261283
284+ this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
285+
262286 if (persist) {
263287 // Build and save the reasoning data to message extras
264288 extra.reasoning = this.reasoning;
265289 extra.reasoning_duration = this.getDuration();
290+ extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
266291 }
267292
268293 return reasoningChanged;
@@ -279,7 +304,10 @@ export class ReasoningHandler {
279304 * @returns {Promise<void>}
280305 */
281306 async process(messageId, mesChanged) {
282- if (!this.reasoning && !this.#isHiddenReasoningModel) return;
307+ mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged);
308+
309+ if (!this.reasoning && !this.#isHiddenReasoningModel)
310+ return;
283311
284312 // Ensure reasoning string is updated and regexes are applied correctly
285313 const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
@@ -294,6 +322,53 @@ export class ReasoningHandler {
294322 }
295323 }
296324
325+ #autoParseReasoningFromMessage(messageId, mesChanged) {
326+ if (!power_user.reasoning.auto_parse)
327+ return;
328+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix)
329+ return mesChanged;
330+
331+ /** @type {{ mes: string, [key: string]: any}} */
332+ const message = chat[messageId];
333+ if (!message) return mesChanged;
334+
335+ // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it.
336+ if (this.#parsingReasoningMesStartIndex) {
337+ message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex));
338+ return mesChanged;
339+ }
340+
341+ if (this.state === ReasoningState.None) {
342+ // If streamed message starts with the opening, cut it out and put all inside reasoning
343+ if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
344+ this.#isParsingReasoning = true;
345+
346+ // Manually set starting state here, as we might already have received the ending suffix
347+ this.state = ReasoningState.Thinking;
348+ this.startTime = this.initialTime;
349+ }
350+ }
351+
352+ if (!this.#isParsingReasoning)
353+ return mesChanged;
354+
355+ // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block
356+ const originalMes = message.mes;
357+ this.reasoning = originalMes.slice(power_user.reasoning.prefix.length);
358+ message.mes = '';
359+
360+ // If the reasoning contains the ending suffix, we cut that off and continue as message streaming
361+ if (this.reasoning.includes(power_user.reasoning.suffix)) {
362+ this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix));
363+ this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length;
364+ message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex));
365+ this.#isParsingReasoning = false;
366+ }
367+
368+ // Only return the original mesChanged value if we haven't cut off the complete message
369+ return message.mes.length ? mesChanged : false;
370+ }
371+
297372 /**
298373 * Completes the reasoning process for a message.
299374 *
@@ -336,9 +411,10 @@ export class ReasoningHandler {
336411 // Update states to the relevant DOM elements
337412 setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
338413 setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
414+ setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
339415
340416 // Update the reasoning message
341417 const reasoning = power_user.trim_spaces ? this.reasoning.trimtrimSpaces() : this.reasoning);
342418 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
343419 this.messageReasoningContentDom.innerHTML = displayReasoning;
344420
@@ -393,17 +469,14 @@ export class ReasoningHandler {
393469 const element = this.messageReasoningHeaderDom;
394470 const duration = this.getDuration();
395471 let data = null;
472+ let title = '';
396473 if (duration) {
397474 const durationStrseconds = moment.duration(duration).locale(getCurrentLocale()).humanizeasSeconds({ s: 50, ss: 3 });
398- const secondsStr = moment.duration(duration).asSeconds();
399-
400- const span = document.createElement('span');
401- span.title = t`${secondsStr} seconds`;
402- span.textContent = durationStr;
403475
404- element.textContent = t`Thought for `;
476+ const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
405- element.appendChild(span);
477+ element.textContent = t`Thought for ${durationStr}`;
406478 data = String(secondsStrseconds);
479+ title = `${seconds} seconds`;
407480 } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
408481 element.textContent = t`Thought for some time`;
409482 data = 'unknown';
@@ -412,6 +485,12 @@ export class ReasoningHandler {
412485 data = null;
413486 }
414487
488+ if (this.type !== ReasoningType.Model) {
489+ title += ` [${translate(this.type)}]`;
490+ title = title.trim();
491+ }
492+ element.title = title;
493+
415494 setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
416495 setDatasetProperty(element, 'duration', data);
417496 }
@@ -573,11 +652,16 @@ function registerReasoningSlashCommands() {
573652 callback: async (args, value) => {
574653 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
575654 const message = chat[messageId];
576655 if (!message?.extra) {
577656 return '';
578657 }
658+ // Make sure the message has an extra object
659+ if (!message.extra || typeof message.extra !== 'object') {
660+ message.extra = {};
661+ }
579662
580663 message.extra.reasoning = String(value ?? '');
664+ message.extra.reasoning_type = ReasoningType.Manual;
581665 await saveChatConditional();
582666
583667 closeMessageEditor('reasoning');
@@ -598,7 +682,26 @@ function registerReasoningSlashCommands() {
598682 typeList: [ARGUMENT_TYPE.BOOLEAN],
599683 defaultValue: 'true',
600684 isRequired: false,
601685 enumProviderenumList: commonEnumProviders.boolean('trueFalse')(),
686+ }),
687+ SlashCommandNamedArgument.fromProps({
688+ name: 'return',
689+ description: 'Whether to return the parsed reasoning or the content without reasoning',
690+ typeList: [ARGUMENT_TYPE.STRING],
691+ defaultValue: 'reasoning',
692+ isRequired: false,
693+ enumList: [
694+ new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning),
695+ new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message),
696+ ],
697+ }),
698+ SlashCommandNamedArgument.fromProps({
699+ name: 'strict',
700+ description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).',
701+ typeList: [ARGUMENT_TYPE.BOOLEAN],
702+ defaultValue: 'true',
703+ isRequired: false,
704+ enumList: commonEnumProviders.boolean('trueFalse')(),
602705 }),
603706 ],
604707 unnamedArgumentList: [
@@ -608,19 +711,27 @@ function registerReasoningSlashCommands() {
608711 }),
609712 ],
610713 callback: (args, value) => {
611714 if (!value || typeof value !== 'string') {
612715 return '';
613716 }
614717
615718 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
616719 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
617720 return String(value);
721+ }
722+ if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) {
723+ toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`);
618724 }
619725
620- const parsedReasoning = parseReasoningFromString(String(value));
726+ const returnMessage = args.return === 'content';
621727
728+ const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
622729 if (!parsedReasoning) {
623730 return returnMessage ? value : '';
731+ }
732+
733+ if (returnMessage) {
734+ return parsedReasoning.content;
624735 }
625736
626737 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
@@ -720,6 +831,7 @@ function setReasoningEventHandlers() {
720831 const textarea = messageBlock.find('.reasoning_edit_textarea');
721832 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
722833 message.extra.reasoning = reasoning;
834+ message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
723835 await saveChatConditional();
724836 updateMessageBlock(messageId, message);
725837 textarea.remove();
@@ -780,6 +892,8 @@ function setReasoningEventHandlers() {
780892 return;
781893 }
782894 message.extra.reasoning = '';
895+ delete message.extra.reasoning_type;
896+ delete message.extra.reasoning_duration;
783897 await saveChatConditional();
784898 updateMessageBlock(messageId, message);
785899 const textarea = messageBlock.find('.reasoning_edit_textarea');
@@ -819,16 +933,18 @@ export function removeReasoningFromString(str) {
819933 * @property {string} reasoning Reasoning block
820934 * @property {string} content Message content
821935 * @param {string} str Content of the message
936+ * @param {Object} options Optional arguments
937+ * @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
822938 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
823939 */
824940function parseReasoningFromString(str, { strict = true } = {}) {
825941 // Both prefix and suffix must be defined
826942 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
827943 return null;
828944 }
829945
830946 try {
831947 const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
832948
833949 let didReplace = false;
834950 let reasoning = '';
@@ -838,9 +954,9 @@ function parseReasoningFromString(str) {
838954 return '';
839955 });
840956
841957 if (didReplace && power_user.trim_spaces) {
842958 reasoning = reasoning.trimtrimSpaces(reasoning);
843959 content = content.trimtrimSpaces(content);
844960 }
845961
846962 return { reasoning, content };
@@ -869,6 +985,11 @@ function registerReasoningAppEvents() {
869985 return null;
870986 }
871987
988+ if (message.extra?.reasoning) {
989+ console.debug('[Reasoning] Message already has reasoning', idx);
990+ return null;
991+ }
992+
872993 const parsedReasoning = parseReasoningFromString(message.mes);
873994
874995 // No reasoning block found
@@ -886,6 +1007,7 @@ function registerReasoningAppEvents() {
8861007 // If reasoning was found, add it to the message
8871008 if (parsedReasoning.reasoning) {
8881009 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
1010+ message.extra.reasoning_type = ReasoningType.Parsed;
8891011 }
8901012
8911013 // Update the message text if it was changed
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -34,6 +34,7 @@ export const enumIcons = {
3434 preset: '⚙️',
3535 file: '📄',
3636 message: '💬',
37+ reasoning: '💡',
3738 voice: '🎤',
3839 server: '🖥️',
3940 popup: '🗔',
public/scripts/textgen-settings.js+2 -2
@@ -311,7 +311,7 @@ export function validateTextGenUrl() {
311311 const formattedUrl = formatTextGenURL(url);
312312
313313 if (!formattedUrl) {
314314 toastr.error('t`Enter a valid API URL'`, 'Text Completion API');
315315 return;
316316 }
317317
@@ -1187,7 +1187,7 @@ export function getTextGenModel() {
11871187 return settings.aphrodite_model;
11881188 case OLLAMA:
11891189 if (!settings.ollama_model) {
11901190 toastr.error('t`No Ollama model selected.'`, 'Text Completion API');
11911191 throw new Error('No Ollama model selected');
11921192 }
11931193 return settings.ollama_model;
public/scripts/utils.js+14 -1
@@ -8,7 +8,7 @@ import {
88import { getContext } from './extensions.js';
99import { characters, getRequestHeaders, this_chid } from '../script.js';
1010import { isMobile } from './RossAscends-mods.js';
1111import { collapseNewlines, power_user } from './power-user.js';
1212import { debounce_timeout } from './constants.js';
1313import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
1414import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -677,6 +677,19 @@ export function sortByCssOrder(a, b) {
677677}
678678
679679/**
680+ * Trims leading and trailing whitespace from the input string based on a configuration setting.
681+ * @param {string} input - The string to be trimmed
682+ * @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string
683+ */
684+
685+export function trimSpaces(input) {
686+ if (!input || typeof input !== 'string') {
687+ return input;
688+ }
689+ return power_user.trim_spaces ? input.trim() : input;
690+}
691+
692+/**
680693 * Trims a string to the end of a nearest sentence.
681694 * @param {string} input The string to trim.
682695 * @returns {string} The trimmed string.
src/endpoints/vectors.js+11 -11
@@ -132,35 +132,35 @@ function getSourceSettings(source, request) {
132132 switch (source) {
133133 case 'togetherai':
134134 return {
135135 model: String(request.headers['x-togetherai-body.model']),
136136 };
137137 case 'openai':
138138 return {
139139 model: String(request.headers['x-openai-body.model']),
140140 };
141141 case 'cohere':
142142 return {
143143 model: String(request.headers['x-cohere-body.model']),
144144 };
145145 case 'llamacpp':
146146 return {
147147 apiUrl: String(request.headers['x-llamacpp-url']body.apiUrl),
148148 };
149149 case 'vllm':
150150 return {
151151 apiUrl: String(request.headers['x-vllm-url']body.apiUrl),
152152 model: String(request.headers['x-vllm-body.model']),
153153 };
154154 case 'ollama':
155155 return {
156156 apiUrl: String(request.headers['x-ollama-url']body.apiUrl),
157157 model: String(request.headers['x-ollama-body.model']),
158158 keep: Boolean(request.headers['x-ollama-body.keep']),
159159 };
160160 case 'extras':
161161 return {
162162 extrasUrl: String(request.headers['x-extras-url']body.extrasUrl),
163163 extrasKey: String(request.headers['x-extras-key']body.extrasKey),
164164 };
165165 case 'transformers':
166166 return {