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 @@
87 "@types/cookie-session": "^2.0.49",87 "@types/cookie-session": "^2.0.49",
88 "@types/cors": "^2.8.17",88 "@types/cors": "^2.8.17",
89 "@types/deno": "^2.0.0",89 "@types/deno": "^2.0.0",
90 "@types/dompurify": "^3.2.0",
91 "@types/express": "^4.17.21",90 "@types/express": "^4.17.21",
92 "@types/jquery": "^3.5.29",91 "@types/jquery": "^3.5.29",
93 "@types/jquery-cropper": "^1.0.4",92 "@types/jquery-cropper": "^1.0.4",
@@ -1181,17 +1180,6 @@
1181 "dev": true,1180 "dev": true,
1182 "license": "MIT"1181 "license": "MIT"
1183 },1182 },
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 },
1195 "node_modules/@types/estree": {1183 "node_modules/@types/estree": {
1196 "version": "1.0.6",1184 "version": "1.0.6",
1197 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",1185 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
package.json+0 -1
@@ -116,7 +116,6 @@
116 "@types/cookie-session": "^2.0.49",116 "@types/cookie-session": "^2.0.49",
117 "@types/cors": "^2.8.17",117 "@types/cors": "^2.8.17",
118 "@types/deno": "^2.0.0",118 "@types/deno": "^2.0.0",
119 "@types/dompurify": "^3.2.0",
120 "@types/express": "^4.17.21",119 "@types/express": "^4.17.21",
121 "@types/jquery": "^3.5.29",120 "@types/jquery": "^3.5.29",
122 "@types/jquery-cropper": "^1.0.4",121 "@types/jquery-cropper": "^1.0.4",
public/index.html+4 -4
@@ -2015,7 +2015,7 @@
2015 </div>2015 </div>
2016 </div>2016 </div>
2017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">2017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">
2018 <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.">2018 <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.">
2019 <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">2019 <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
2020 Reasoning Effort2020 Reasoning Effort
2021 </label>2021 </label>
@@ -4001,7 +4001,7 @@
4001 <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">4001 <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">
4002 <small>4002 <small>
4003 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>4003 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
4004 <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>4004 <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>
4005 </small>4005 </small>
4006 <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">4006 <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">
4007 <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">4007 <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 @@
6890 </div>6890 </div>
6891 <div id="form_sheld">6891 <div id="form_sheld">
6892 <div id="dialogue_del_mes">6892 <div id="dialogue_del_mes">
6893 <div id="dialogue_del_mes_ok" class="menu_button">Delete</div>6893 <div id="dialogue_del_mes_ok" data-i18n="Delete" class="menu_button">Delete</div>
6894 <div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div>6894 <div id="dialogue_del_mes_cancel" data-i18n="Cancel" class="menu_button">Cancel</div>
6895 </div>6895 </div>
6896 <div id="send_form" class="no-connection">6896 <div id="send_form" class="no-connection">
6897 <form id="file_form" class="wide100p displayNone">6897 <form id="file_form" class="wide100p displayNone">
public/locales/fr-fr.json+1 -1
@@ -1485,7 +1485,7 @@
1485 "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",1485 "(disabled when max recursion steps are used)": "(désactivé lorsque le nombre maximum de pas de récursivité est utilisé)",
1486 "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",1486 "Cap the number of entry activation recursions": "Plafonner le nombre de récursions d'activation d'entrée",
1487 "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",1487 "Max Recursion Steps": "Nombre maximal d'étapes de récursivité",
1488 "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)",1488 "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)",
1489 "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",1489 "Include names with each message into the context for scanning": "Inclure les noms dans chaque message dans le contexte pour l'analyse.",
1490 "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",1490 "Apply current sorting as Order": "Appliquer le tri actuel comme ordre",
1491 "Display swipe numbers for all messages, not just the last.": "Afficher le nombre de balayage sur tous les messages, et pas seulement le dernier.",1491 "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 @@
195 "Yes": "Да",195 "Yes": "Да",
196 "No": "Нет",196 "No": "Нет",
197 "Context %": "Процент контекста",197 "Context %": "Процент контекста",
198 "Budget Cap": "Бюджетный лимит",198 "Budget Cap": "Лимит бюджета",
199 "(0 = disabled)": "(0 = отключено)",199 "(0 = disabled)": "(0 = отключено)",
200 "None": "Отсутствует",200 "None": "Отсутствует",
201 "User Settings": "Настройки пользователя",201 "User Settings": "Настройки пользователя",
@@ -575,10 +575,10 @@
575 "Characters sorting order": "Порядок сортировки персонажей",575 "Characters sorting order": "Порядок сортировки персонажей",
576 "Remove": "Убрать",576 "Remove": "Убрать",
577 "Select a World Info file for": "Выбрать файл с миром для",577 "Select a World Info file for": "Выбрать файл с миром для",
578 "Primary Lorebook": "Основного лорбука",578 "Primary Lorebook": "Основной лорбук",
579 "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук",579 "A selected World Info will be bound to this character as its own Lorebook.": "Информация о мире будет привязана к персонажу как его собственный лорбук.",
580 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира",580 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "Когда ИИ генерирует ответ, он будет совмещён с записями из глобально выбранного мира.",
581 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON",581 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "При экспорте персонажа вместе с ним также выгрузится выбранный лорбук в виде JSON.",
582 "Additional Lorebooks": "Вспомогательные лорбуки",582 "Additional Lorebooks": "Вспомогательные лорбуки",
583 "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",583 "Associate one or more auxillary Lorebooks with this character.": "Привязать к этому персонажу один или больше вспомогательных лорбуков",
584 "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",584 "NOTE: These choices are optional and won't be preserved on character export!": "ВНИМАНИЕ: эти выборы необязательные и не будут сохранены при экспорте персонажа!",
@@ -593,7 +593,7 @@
593 "Prompt": "Промпт",593 "Prompt": "Промпт",
594 "Copy": "Скопировать",594 "Copy": "Скопировать",
595 "Confirm": "Подтвердить",595 "Confirm": "Подтвердить",
596 "Copy this message": "Скопировать сообщение",596 "Copy this message": "Продублировать сообщение",
597 "Delete this message": "Удалить сообщение",597 "Delete this message": "Удалить сообщение",
598 "Move message up": "Переместить сообщение вверх",598 "Move message up": "Переместить сообщение вверх",
599 "Move message down": "Переместить сообщение вниз",599 "Move message down": "Переместить сообщение вниз",
@@ -612,7 +612,7 @@
612 "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",612 "Ask AI to write your message for you": "Попросить ИИ написать сообщение за вас",
613 "Continue the last message": "Продолжить текущее сообщение",613 "Continue the last message": "Продолжить текущее сообщение",
614 "Bind user name to that avatar": "Закрепить имя за этим аватаром",614 "Bind user name to that avatar": "Закрепить имя за этим аватаром",
615 "Select this as default persona for the new chats.": "Выберать эту Персону в качестве персоны по умолчанию для новых чатов.",615 "Select this as default persona for the new chats.": "Выбирать эту персону по умолчанию для всех новых чатов.",
616 "Change persona image": "Сменить аватар персоны",616 "Change persona image": "Сменить аватар персоны",
617 "Delete persona": "Удалить персону",617 "Delete persona": "Удалить персону",
618 "Reduced Motion": "Сокращение анимаций",618 "Reduced Motion": "Сокращение анимаций",
@@ -640,7 +640,7 @@
640 "Token Probabilities": "Вероятности токенов",640 "Token Probabilities": "Вероятности токенов",
641 "Close chat": "Закрыть чат",641 "Close chat": "Закрыть чат",
642 "Manage chat files": "Все чаты",642 "Manage chat files": "Все чаты",
643 "Import Extension From Git Repo": "Импортировать расширение из Git Repository",643 "Import Extension From Git Repo": "Импортировать расширение из Git-репозитория.",
644 "Install extension": "Установить расширение",644 "Install extension": "Установить расширение",
645 "Manage extensions": "Управление расширениями",645 "Manage extensions": "Управление расширениями",
646 "Tokens persona description": "Токенов",646 "Tokens persona description": "Токенов",
@@ -1122,7 +1122,7 @@
1122 "help_hotkeys_0": "Горячие клавиши",1122 "help_hotkeys_0": "Горячие клавиши",
1123 "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",1123 "You can browse a list of bundled characters in the": "Комплектных персонажей можно найти в меню",
1124 "Download Extensions & Assets": "Загрузить расширения и ресурсы",1124 "Download Extensions & Assets": "Загрузить расширения и ресурсы",
1125 "menu within": "внутри этих кубиков",1125 "menu within": "в меню",
1126 "Assets URL": "URL с описанием ресурсов",1126 "Assets URL": "URL с описанием ресурсов",
1127 "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",1127 "Custom (OpenAI-compatible)": "Кастомный (совместимый с OpenAI)",
1128 "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",1128 "Custom Endpoint (Base URL)": "Кастомный эндпоинт (базовый URL)",
@@ -1943,7 +1943,7 @@
1943 "and connect to an": "и подключитесь к",1943 "and connect to an": "и подключитесь к",
1944 "You can add more": "Можете добавить больше",1944 "You can add more": "Можете добавить больше",
1945 "from other websites": "с других сайтов.",1945 "from other websites": "с других сайтов.",
1946 "Go to the": "Загляните в",1946 "Go to the": "Заходите в",
1947 "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",1947 "to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
1948 "or_welcome": "; также доступен",1948 "or_welcome": "; также доступен",
1949 "Claude API Key": "Ключ от API Claude",1949 "Claude API Key": "Ключ от API Claude",
@@ -1958,7 +1958,7 @@
1958 "Save": "Сохранить",1958 "Save": "Сохранить",
1959 "Chat Lorebook": "Лорбук для чата",1959 "Chat Lorebook": "Лорбук для чата",
1960 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",1960 "chat_world_template_txt": "Выбранный мир будет привязан к этому чату. Будет добавляться в промпт наряду с глобальным лорбуком и лором персонажа.",
1961 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + клик, чтобы открыть диалог привязки мира",1961 "world_button_title": "Лор персонажа\n\nНажмите, чтобы загрузить\nShift + ЛКМ, чтобы открыть диалог привязки мира",
1962 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",1962 "No auxillary Lorebooks set. Click here to select.": "Вспомогательный лорбук не выбран. Нажмите, чтобы выбрать.",
1963 "ext_regex_user_input_desc": "Отправленные вами сообщения.",1963 "ext_regex_user_input_desc": "Отправленные вами сообщения.",
1964 "ext_regex_ai_input_desc": "Полученные от API ответы.",1964 "ext_regex_ai_input_desc": "Полученные от API ответы.",
@@ -2144,5 +2144,65 @@
2144 "Not connected to the API!": "Нет соединения с API!",2144 "Not connected to the API!": "Нет соединения с API!",
2145 "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",2145 "ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
2146 "Update all": "Обновить все",2146 "Update all": "Обновить все",
2147 "Close": "Закрыть"2147 "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:": "Токенов:"
2148}2208}
2148 \ No newline at end of file2208 \ No newline at end of file
public/locales/zh-cn.json+1 -1
@@ -584,7 +584,7 @@
584 "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)",584 "(0 = unlimited, use budget)": "(“0”为无限制,使用预算)",
585 "Cap the number of entry activation recursions": "限制条目激活递归的次数",585 "Cap the number of entry activation recursions": "限制条目激活递归的次数",
586 "Max Recursion Steps": "最大递归深度",586 "Max Recursion Steps": "最大递归深度",
587 "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(当使用最小激活次数时,此功能被禁用)",587 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "“0”为无限制,“1”为扫描一次且不递归,“2”为扫描一次且递归一次,依此类推\n(当使用最小激活次数时,此功能被禁用)",
588 "Insertion Strategy": "插入策略",588 "Insertion Strategy": "插入策略",
589 "Sorted Evenly": "均匀排序",589 "Sorted Evenly": "均匀排序",
590 "Character Lore First": "角色世界书优先",590 "Character Lore First": "角色世界书优先",
public/locales/zh-tw.json+2 -2
@@ -1458,7 +1458,7 @@
1458 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",1458 "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",
1459 "popup-button-crop": "裁剪",1459 "popup-button-crop": "裁剪",
1460 "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",1460 "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
1461 "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(使用最小啟動設定時將停用)",1461 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)",
1462 "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)。",1462 "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)。",
1463 "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",1463 "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用該擴充功能區域的倍數。",
1464 "Abort current image generation task": "終止目前的圖片生成任務",1464 "Abort current image generation task": "終止目前的圖片生成任務",
@@ -1806,7 +1806,7 @@
1806 "context_derived": "若可能,根據模型元數據推導。",1806 "context_derived": "若可能,根據模型元數據推導。",
1807 "instruct_derived": "若可能,根據模型元數據推導。",1807 "instruct_derived": "若可能,根據模型元數據推導。",
1808 "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",1808 "Inserted before the first User's message.": "插入於第一則使用者訊息之前。",
1809 "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(啟用最小啟動次數時無效)",1809 "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc": "0 = 無限制,1 = 掃描一次不遞歸,2 = 掃描一次後遞歸一次 ⋯以此類推\n(啟用最小啟動次數時無效)",
1810 "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",1810 "Quick 'Impersonate' button": "快速「AI 扮演使用者」按鈕",
1811 "Manual": "手動",1811 "Manual": "手動",
1812 "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)",1812 "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) => {
366 return;366 return;
367 }367 }
368368
369 if (!(node instanceof Element)) {
370 return;
371 }
372
369 let mediaBlocked = false;373 let mediaBlocked = false;
370374
371 switch (node.tagName) {375 switch (node.tagName) {
@@ -3234,6 +3238,7 @@ class StreamingProcessor {
32343238
3235 // Update reasoning3239 // Update reasoning
3236 await this.reasoningHandler.process(messageId, mesChanged);3240 await this.reasoningHandler.process(messageId, mesChanged);
3241 processedText = chat[messageId]['mes'];
32373242
3238 // Token count update.3243 // Token count update.
3239 const tokenCountText = this.reasoningHandler.reasoning + processedText;3244 const tokenCountText = this.reasoningHandler.reasoning + processedText;
@@ -3384,7 +3389,7 @@ class StreamingProcessor {
3384 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3389 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3385 }3390 }
3386 // Get the updated reasoning string into the handler3391 // Get the updated reasoning string into the handler
3387 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');3392 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
3388 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3393 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3389 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));3394 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
3390 }3395 }
@@ -10984,7 +10989,7 @@ jQuery(async function () {
10984 });10989 });
1098510990
10986 $(document).on('click', '.mes_edit_copy', async function () {10991 $(document).on('click', '.mes_edit_copy', async function () {
10987 const confirmation = await callGenericPopup('Create a copy of this message?', POPUP_TYPE.CONFIRM);10992 const confirmation = await callGenericPopup(t`Create a copy of this message?`, POPUP_TYPE.CONFIRM);
10988 if (!confirmation) {10993 if (!confirmation) {
10989 return;10994 return;
10990 }10995 }
public/scripts/extensions.js+4 -4
@@ -603,12 +603,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
603 }603 }
604604
605 let toggleElement = isActive || isDisabled ?605 let toggleElement = isActive || isDisabled ?
606 `<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :606 '<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
607 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;607 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
608608
609 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>` : '';609 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>` : '';
610 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>` : '';610 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>` : '';
611 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>` : '';611 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>` : '';
612 let modulesInfo = '';612 let modulesInfo = '';
613613
614 if (isActive && Array.isArray(manifest.optional)) {614 if (isActive && Array.isArray(manifest.optional)) {
@@ -616,7 +616,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
616 modules.forEach(x => optional.delete(x));616 modules.forEach(x => optional.delete(x));
617 if (optional.size > 0) {617 if (optional.size > 0) {
618 const optionalString = DOMPurify.sanitize([...optional].join(', '));618 const optionalString = DOMPurify.sanitize([...optional].join(', '));
619 modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`;619 modulesInfo = '<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
620 }620 }
621 } else if (!isDisabled) { // Neither active nor disabled621 } else if (!isDisabled) { // Neither active nor disabled
622 const requirements = new Set(manifest.requires);622 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';
10import { executeSlashCommands } from '../../slash-commands.js';10import { executeSlashCommands } from '../../slash-commands.js';
11import { accountStorage } from '../../util/AccountStorage.js';11import { accountStorage } from '../../util/AccountStorage.js';
12import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';12import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
13import { t } from '../../i18n.js';
13export { MODULE_NAME };14export { MODULE_NAME };
1415
15const MODULE_NAME = 'assets';16const MODULE_NAME = 'assets';
@@ -59,11 +60,11 @@ const KNOWN_TYPES = {
59 'blip': 'Blip sounds',60 'blip': 'Blip sounds',
60};61};
6162
62function downloadAssetsList(url) {63async function downloadAssetsList(url) {
63 updateCurrentAssets().then(function () {64 updateCurrentAssets().then(async function () {
64 fetch(url, { cache: 'no-cache' })65 fetch(url, { cache: 'no-cache' })
65 .then(response => response.json())66 .then(response => response.json())
66 .then(json => {67 .then(async function(json) {
6768
68 availableAssets = {};69 availableAssets = {};
69 $('#assets_menu').empty();70 $('#assets_menu').empty();
@@ -84,10 +85,10 @@ function downloadAssetsList(url) {
8485
85 $('#assets_type_select').empty();86 $('#assets_type_select').empty();
86 $('#assets_search').val('');87 $('#assets_search').val('');
87 $('#assets_type_select').append($('<option />', { value: '', text: 'All' }));88 $('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
8889
89 for (const type of assetTypes) {90 for (const type of assetTypes) {
90 const option = $('<option />', { value: type, text: KNOWN_TYPES[type] || type });91 const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
91 $('#assets_type_select').append(option);92 $('#assets_type_select').append(option);
92 }93 }
9394
@@ -104,11 +105,7 @@ function downloadAssetsList(url) {
104 assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();105 assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
105106
106 if (assetType == 'extension') {107 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>`);
112 }109 }
113110
114 for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {111 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) {
184 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);181 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
185 const description = DOMPurify.sanitize(asset['description'] || '');182 const description = DOMPurify.sanitize(asset['description'] || '');
186 const url = isValidUrl(asset['url']) ? asset['url'] : '';183 const url = isValidUrl(asset['url']) ? asset['url'] : '';
187 const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';184 const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
188 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';185 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
189 const toolTag = assetType === 'extension' && asset['tool'];186 const toolTag = assetType === 'extension' && asset['tool'];
190187
@@ -195,9 +192,10 @@ function downloadAssetsList(url) {
195 <b>${displayName}</b>192 <b>${displayName}</b>
196 <a class="asset_preview" href="${url}" target="_blank" title="${title}">193 <a class="asset_preview" href="${url}" target="_blank" title="${title}">
197 <i class="fa-solid fa-sm ${previewIcon}"></i>194 <i class="fa-solid fa-sm ${previewIcon}"></i>
198 </a>195 </a>` +
199 ${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}196 (toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' +
200 </span>197 t`Tool` + '</span>' : '') +
198 `</span>
201 <small class="asset-description">199 <small class="asset-description">
202 ${description}200 ${description}
203 </small>201 </small>
@@ -435,7 +433,7 @@ jQuery(async () => {
435 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;433 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
436 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';434 const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
437435
438 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>`, {436 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>`, {
439 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],437 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
440 onClose: popup => {438 onClose: popup => {
441 if (popup.result) {439 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>
\ No newline at end of file4 \ 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;
33 <div id="assets_filters" class="flex-container">33 <div id="assets_filters" class="flex-container">
34 <select id="assets_type_select" class="text_pole flex1">34 <select id="assets_type_select" class="text_pole flex1">
35 </select>35 </select>
36 <input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">36 <input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
37 <div id="assets-characters-button" class="menu_button menu_button_icon">37 <div id="assets-characters-button" class="menu_button menu_button_icon">
38 <i class="fa-solid fa-image-portrait"></i>38 <i class="fa-solid fa-image-portrait"></i>
39 <span data-i18n="Characters">Characters</span>39 <span data-i18n="Characters">Characters</span>
public/scripts/extensions/token-counter/index.js+6 -20
@@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
6import { resetScrollHeight, debounce } from '../../utils.js';6import { resetScrollHeight, debounce } from '../../utils.js';
7import { debounce_timeout } from '../../constants.js';7import { debounce_timeout } from '../../constants.js';
8import { POPUP_TYPE, callGenericPopup } from '../../popup.js';8import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
9import { renderExtensionTemplateAsync } from '../../extensions.js';
10import { t } from '../../i18n.js';
911
10function rgb2hex(rgb) {12function rgb2hex(rgb) {
11 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);13 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@@ -22,23 +24,7 @@ $('button').click(function () {
2224
23async function doTokenCounter() {25async function doTokenCounter() {
24 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);26 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
43 const dialog = $(html);29 const dialog = $(html);
44 const countDebounced = debounce(async () => {30 const countDebounced = debounce(async () => {
@@ -131,9 +117,9 @@ async function doCount() {
131jQuery(() => {117jQuery(() => {
132 const buttonHtml = `118 const buttonHtml = `
133 <div id="token_counter" class="list-group-item flex-container flexGap5">119 <div id="token_counter" class="list-group-item flex-container flexGap5">
134 <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>120 <div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
135 Token Counter121 t`Token Counter` +
136 </div>`;122 '</div>';
137 $('#token_counter_wand_container').append(buttonHtml);123 $('#token_counter_wand_container').append(buttonHtml);
138 $('#token_counter').on('click', doTokenCounter);124 $('#token_counter').on('click', doTokenCounter);
139 SlashCommandParser.addCommandObject(SlashCommand.fromProps({125 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>
\ No newline at end of file16 \ No newline at end of file
public/scripts/extensions/vectors/index.js+56 -62
@@ -746,74 +746,65 @@ async function getQueryText(chat, initiator) {
746}746}
747747
748/**748/**
749 * Gets the saved hashes for a collection749 * Gets common body parameters for vector requests.
750* @param {string} collectionId750 * @returns {object}
751* @returns {Promise<number[]>} Saved hashes
752 */751 */
753async function getSavedHashes(collectionId) {752function getVectorsRequestBody() {
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
771function getVectorHeaders() {
772 const headers = getRequestHeaders();
773 switch (settings.source) {754 switch (settings.source) {
774 case 'extras':755 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 });
779 break;758 break;
780 case 'togetherai':759 case 'togetherai':
781 Object.assign(headers, {760 body.model = extension_settings.vectors.togetherai_model;
782 'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
783 });
784 break;761 break;
785 case 'openai':762 case 'openai':
786 Object.assign(headers, {763 body.model = extension_settings.vectors.openai_model;
787 'X-OpenAI-Model': extension_settings.vectors.openai_model,
788 });
789 break;764 break;
790 case 'cohere':765 case 'cohere':
791 Object.assign(headers, {766 body.model = extension_settings.vectors.cohere_model;
792 'X-Cohere-Model': extension_settings.vectors.cohere_model,
793 });
794 break;767 break;
795 case 'ollama':768 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 });
801 break;772 break;
802 case 'llamacpp':773 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 });
806 break;775 break;
807 case 'vllm':776 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 });
812 break;779 break;
813 default:780 default:
814 break;781 break;
815 }782 }
816 return headers;783 return body;
784}
785
786/**
787 * Gets the saved hashes for a collection
788* @param {string} collectionId
789* @returns {Promise<number[]>} Saved hashes
790*/
791async 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;
817}808}
818809
819/**810/**
@@ -825,12 +816,11 @@ function getVectorHeaders() {
825async function insertVectorItems(collectionId, items) {816async function insertVectorItems(collectionId, items) {
826 throwIfSourceInvalid();817 throwIfSourceInvalid();
827818
828 const headers = getVectorHeaders();
829
830 const response = await fetch('/api/vector/insert', {819 const response = await fetch('/api/vector/insert', {
831 method: 'POST',820 method: 'POST',
832 headers: headers,821 headers: getRequestHeaders(),
833 body: JSON.stringify({822 body: JSON.stringify({
823 ...getVectorsRequestBody(),
834 collectionId: collectionId,824 collectionId: collectionId,
835 items: items,825 items: items,
836 source: settings.source,826 source: settings.source,
@@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
879async function deleteVectorItems(collectionId, hashes) {869async function deleteVectorItems(collectionId, hashes) {
880 const response = await fetch('/api/vector/delete', {870 const response = await fetch('/api/vector/delete', {
881 method: 'POST',871 method: 'POST',
882 headers: getVectorHeaders(),872 headers: getRequestHeaders(),
883 body: JSON.stringify({873 body: JSON.stringify({
874 ...getVectorsRequestBody(),
884 collectionId: collectionId,875 collectionId: collectionId,
885 hashes: hashes,876 hashes: hashes,
886 source: settings.source,877 source: settings.source,
@@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
899 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results890 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
900 */891 */
901async function queryCollection(collectionId, searchText, topK) {892async function queryCollection(collectionId, searchText, topK) {
902 const headers = getVectorHeaders();
903
904 const response = await fetch('/api/vector/query', {893 const response = await fetch('/api/vector/query', {
905 method: 'POST',894 method: 'POST',
906 headers: headers,895 headers: getRequestHeaders(),
907 body: JSON.stringify({896 body: JSON.stringify({
897 ...getVectorsRequestBody(),
908 collectionId: collectionId,898 collectionId: collectionId,
909 searchText: searchText,899 searchText: searchText,
910 topK: topK,900 topK: topK,
@@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
929 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs919 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
930 */920 */
931async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {921async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
932 const headers = getVectorHeaders();
933
934 const response = await fetch('/api/vector/query-multi', {922 const response = await fetch('/api/vector/query-multi', {
935 method: 'POST',923 method: 'POST',
936 headers: headers,924 headers: getRequestHeaders(),
937 body: JSON.stringify({925 body: JSON.stringify({
926 ...getVectorsRequestBody(),
938 collectionIds: collectionIds,927 collectionIds: collectionIds,
939 searchText: searchText,928 searchText: searchText,
940 topK: topK,929 topK: topK,
@@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
965954
966 const response = await fetch('/api/vector/purge', {955 const response = await fetch('/api/vector/purge', {
967 method: 'POST',956 method: 'POST',
968 headers: getVectorHeaders(),957 headers: getRequestHeaders(),
969 body: JSON.stringify({958 body: JSON.stringify({
959 ...getVectorsRequestBody(),
970 collectionId: collectionId,960 collectionId: collectionId,
971 }),961 }),
972 });962 });
@@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
994984
995 const response = await fetch('/api/vector/purge', {985 const response = await fetch('/api/vector/purge', {
996 method: 'POST',986 method: 'POST',
997 headers: getVectorHeaders(),987 headers: getRequestHeaders(),
998 body: JSON.stringify({988 body: JSON.stringify({
989 ...getVectorsRequestBody(),
999 collectionId: collectionId,990 collectionId: collectionId,
1000 }),991 }),
1001 });992 });
@@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
1019 try {1010 try {
1020 const response = await fetch('/api/vector/purge-all', {1011 const response = await fetch('/api/vector/purge-all', {
1021 method: 'POST',1012 method: 'POST',
1022 headers: getVectorHeaders(),1013 headers: getRequestHeaders(),
1014 body: JSON.stringify({
1015 ...getVectorsRequestBody(),
1016 }),
1023 });1017 });
10241018
1025 if (!response.ok) {1019 if (!response.ok) {
public/scripts/reasoning.js+153 -31
@@ -3,17 +3,30 @@ import {
3} from '../lib.js';3} from '../lib.js';
4import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';4import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
6import { getCurrentLocale, t } from './i18n.js';6import { getCurrentLocale, t, translate } from './i18n.js';
7import { MacrosParser } from './macros.js';7import { MacrosParser } from './macros.js';
8import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';8import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
9import { Popup } from './popup.js';9import { Popup } from './popup.js';
10import { power_user } from './power-user.js';10import { power_user } from './power-user.js';
11import { SlashCommand } from './slash-commands/SlashCommand.js';11import { SlashCommand } from './slash-commands/SlashCommand.js';
12import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';12import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
13import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';13import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
14import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';15import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
15import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';16import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
16import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js';17import { 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 */
24export const ReasoningType = {
25 Model: 'model',
26 Parsed: 'parsed',
27 Manual: 'manual',
28 Edited: 'edited',
29};
1730
18/**31/**
19 * Gets a message from a jQuery element.32 * Gets a message from a jQuery element.
@@ -129,7 +142,12 @@ export const ReasoningState = {
129 * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.142 * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
130 */143 */
131export class ReasoningHandler {144export class ReasoningHandler {
145 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
132 #isHiddenReasoningModel;146 #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
134 /**152 /**
135 * @param {Date?} [timeStarted=null] - When the generation started153 * @param {Date?} [timeStarted=null] - When the generation started
@@ -137,6 +155,8 @@ export class ReasoningHandler {
137 constructor(timeStarted = null) {155 constructor(timeStarted = null) {
138 /** @type {ReasoningState} The current state of the reasoning process */156 /** @type {ReasoningState} The current state of the reasoning process */
139 this.state = ReasoningState.None;157 this.state = ReasoningState.None;
158 /** @type {ReasoningType?} The type of the reasoning (where it came from) */
159 this.type = null;
140 /** @type {string} The reasoning output */160 /** @type {string} The reasoning output */
141 this.reasoning = '';161 this.reasoning = '';
142 /** @type {Date} When the reasoning started */162 /** @type {Date} When the reasoning started */
@@ -147,7 +167,6 @@ export class ReasoningHandler {
147 /** @type {Date} Initial starting time of the generation */167 /** @type {Date} Initial starting time of the generation */
148 this.initialTime = timeStarted ?? new Date();168 this.initialTime = timeStarted ?? new Date();
149169
150 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
151 this.#isHiddenReasoningModel = isHiddenReasoningModel();170 this.#isHiddenReasoningModel = isHiddenReasoningModel();
152171
153 // Cached DOM elements for reasoning172 // Cached DOM elements for reasoning
@@ -194,6 +213,7 @@ export class ReasoningHandler {
194 this.state = ReasoningState.Hidden;213 this.state = ReasoningState.Hidden;
195 }214 }
196215
216 this.type = extra?.reasoning_type;
197 this.reasoning = extra?.reasoning ?? '';217 this.reasoning = extra?.reasoning ?? '';
198218
199 if (this.state !== ReasoningState.None) {219 if (this.state !== ReasoningState.None) {
@@ -208,6 +228,7 @@ export class ReasoningHandler {
208 // Make sure reset correctly clears all relevant states228 // Make sure reset correctly clears all relevant states
209 if (reset) {229 if (reset) {
210 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;230 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
231 this.type = null;
211 this.reasoning = '';232 this.reasoning = '';
212 this.initialTime = new Date();233 this.initialTime = new Date();
213 this.startTime = null;234 this.startTime = null;
@@ -237,18 +258,19 @@ export class ReasoningHandler {
237 * Updates the reasoning text/string for a message.258 * Updates the reasoning text/string for a message.
238 *259 *
239 * @param {number} messageId - The ID of the message to update260 * @param {number} messageId - The ID of the message to update
240 * @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning261 * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
241 * @param {Object} [options={}] - Optional arguments262 * @param {Object} [options={}] - Optional arguments
242 * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object263 * @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
243 * @returns {boolean} - Returns true if the reasoning was changed, otherwise false265 * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
244 */266 */
245 updateReasoning(messageId, reasoning = null, { persist = false } = {}) {267 updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
246 if (messageId == -1 || !chat[messageId]) {268 if (messageId == -1 || !chat[messageId]) {
247 return false;269 return false;
248 }270 }
249271
250 reasoning = reasoning ?? this.reasoning;272 reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
251 reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning;273 reasoning = trimSpaces(reasoning);
252274
253 // Ensure the chat extra exists275 // Ensure the chat extra exists
254 if (!chat[messageId].extra) {276 if (!chat[messageId].extra) {
@@ -259,10 +281,13 @@ export class ReasoningHandler {
259 const reasoningChanged = extra.reasoning !== reasoning;281 const reasoningChanged = extra.reasoning !== reasoning;
260 this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);282 this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
261283
284 this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
285
262 if (persist) {286 if (persist) {
263 // Build and save the reasoning data to message extras287 // Build and save the reasoning data to message extras
264 extra.reasoning = this.reasoning;288 extra.reasoning = this.reasoning;
265 extra.reasoning_duration = this.getDuration();289 extra.reasoning_duration = this.getDuration();
290 extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
266 }291 }
267292
268 return reasoningChanged;293 return reasoningChanged;
@@ -279,7 +304,10 @@ export class ReasoningHandler {
279 * @returns {Promise<void>}304 * @returns {Promise<void>}
280 */305 */
281 async process(messageId, mesChanged) {306 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
284 // Ensure reasoning string is updated and regexes are applied correctly312 // Ensure reasoning string is updated and regexes are applied correctly
285 const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });313 const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
@@ -294,6 +322,53 @@ export class ReasoningHandler {
294 }322 }
295 }323 }
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
297 /**372 /**
298 * Completes the reasoning process for a message.373 * Completes the reasoning process for a message.
299 *374 *
@@ -336,9 +411,10 @@ export class ReasoningHandler {
336 // Update states to the relevant DOM elements411 // Update states to the relevant DOM elements
337 setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);412 setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
338 setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);413 setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
414 setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
339415
340 // Update the reasoning message416 // Update the reasoning message
341 const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;417 const reasoning = trimSpaces(this.reasoning);
342 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);418 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
343 this.messageReasoningContentDom.innerHTML = displayReasoning;419 this.messageReasoningContentDom.innerHTML = displayReasoning;
344420
@@ -393,17 +469,14 @@ export class ReasoningHandler {
393 const element = this.messageReasoningHeaderDom;469 const element = this.messageReasoningHeaderDom;
394 const duration = this.getDuration();470 const duration = this.getDuration();
395 let data = null;471 let data = null;
472 let title = '';
396 if (duration) {473 if (duration) {
397 const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });474 const seconds = moment.duration(duration).asSeconds();
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}`;
406 data = String(secondsStr);478 data = String(seconds);
479 title = `${seconds} seconds`;
407 } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {480 } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
408 element.textContent = t`Thought for some time`;481 element.textContent = t`Thought for some time`;
409 data = 'unknown';482 data = 'unknown';
@@ -412,6 +485,12 @@ export class ReasoningHandler {
412 data = null;485 data = null;
413 }486 }
414487
488 if (this.type !== ReasoningType.Model) {
489 title += ` [${translate(this.type)}]`;
490 title = title.trim();
491 }
492 element.title = title;
493
415 setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);494 setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
416 setDatasetProperty(element, 'duration', data);495 setDatasetProperty(element, 'duration', data);
417 }496 }
@@ -573,11 +652,16 @@ function registerReasoningSlashCommands() {
573 callback: async (args, value) => {652 callback: async (args, value) => {
574 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;653 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
575 const message = chat[messageId];654 const message = chat[messageId];
576 if (!message?.extra) {655 if (!message) {
577 return '';656 return '';
578 }657 }
658 // Make sure the message has an extra object
659 if (!message.extra || typeof message.extra !== 'object') {
660 message.extra = {};
661 }
579662
580 message.extra.reasoning = String(value ?? '');663 message.extra.reasoning = String(value ?? '');
664 message.extra.reasoning_type = ReasoningType.Manual;
581 await saveChatConditional();665 await saveChatConditional();
582666
583 closeMessageEditor('reasoning');667 closeMessageEditor('reasoning');
@@ -598,7 +682,26 @@ function registerReasoningSlashCommands() {
598 typeList: [ARGUMENT_TYPE.BOOLEAN],682 typeList: [ARGUMENT_TYPE.BOOLEAN],
599 defaultValue: 'true',683 defaultValue: 'true',
600 isRequired: false,684 isRequired: false,
601 enumProvider: commonEnumProviders.boolean('trueFalse'),685 enumList: 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')(),
602 }),705 }),
603 ],706 ],
604 unnamedArgumentList: [707 unnamedArgumentList: [
@@ -608,19 +711,27 @@ function registerReasoningSlashCommands() {
608 }),711 }),
609 ],712 ],
610 callback: (args, value) => {713 callback: (args, value) => {
611 if (!value) {714 if (!value || typeof value !== 'string') {
612 return '';715 return '';
613 }716 }
614717
615 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {718 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
616 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);719 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
617 return String(value);720 return 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`);
618 }724 }
619725
620 const parsedReasoning = parseReasoningFromString(String(value));726 const returnMessage = args.return === 'content';
621727
728 const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
622 if (!parsedReasoning) {729 if (!parsedReasoning) {
623 return '';730 return returnMessage ? value : '';
731 }
732
733 if (returnMessage) {
734 return parsedReasoning.content;
624 }735 }
625736
626 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));737 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
@@ -720,6 +831,7 @@ function setReasoningEventHandlers() {
720 const textarea = messageBlock.find('.reasoning_edit_textarea');831 const textarea = messageBlock.find('.reasoning_edit_textarea');
721 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });832 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
722 message.extra.reasoning = reasoning;833 message.extra.reasoning = reasoning;
834 message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
723 await saveChatConditional();835 await saveChatConditional();
724 updateMessageBlock(messageId, message);836 updateMessageBlock(messageId, message);
725 textarea.remove();837 textarea.remove();
@@ -780,6 +892,8 @@ function setReasoningEventHandlers() {
780 return;892 return;
781 }893 }
782 message.extra.reasoning = '';894 message.extra.reasoning = '';
895 delete message.extra.reasoning_type;
896 delete message.extra.reasoning_duration;
783 await saveChatConditional();897 await saveChatConditional();
784 updateMessageBlock(messageId, message);898 updateMessageBlock(messageId, message);
785 const textarea = messageBlock.find('.reasoning_edit_textarea');899 const textarea = messageBlock.find('.reasoning_edit_textarea');
@@ -819,16 +933,18 @@ export function removeReasoningFromString(str) {
819 * @property {string} reasoning Reasoning block933 * @property {string} reasoning Reasoning block
820 * @property {string} content Message content934 * @property {string} content Message content
821 * @param {string} str Content of the message935 * @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
822 * @returns {ParsedReasoning|null} Parsed reasoning block and message content938 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
823 */939 */
824function parseReasoningFromString(str) {940function parseReasoningFromString(str, { strict = true } = {}) {
825 // Both prefix and suffix must be defined941 // Both prefix and suffix must be defined
826 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {942 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
827 return null;943 return null;
828 }944 }
829945
830 try {946 try {
831 const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');947 const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
832948
833 let didReplace = false;949 let didReplace = false;
834 let reasoning = '';950 let reasoning = '';
@@ -838,9 +954,9 @@ function parseReasoningFromString(str) {
838 return '';954 return '';
839 });955 });
840956
841 if (didReplace && power_user.trim_spaces) {957 if (didReplace) {
842 reasoning = reasoning.trim();958 reasoning = trimSpaces(reasoning);
843 content = content.trim();959 content = trimSpaces(content);
844 }960 }
845961
846 return { reasoning, content };962 return { reasoning, content };
@@ -869,6 +985,11 @@ function registerReasoningAppEvents() {
869 return null;985 return null;
870 }986 }
871987
988 if (message.extra?.reasoning) {
989 console.debug('[Reasoning] Message already has reasoning', idx);
990 return null;
991 }
992
872 const parsedReasoning = parseReasoningFromString(message.mes);993 const parsedReasoning = parseReasoningFromString(message.mes);
873994
874 // No reasoning block found995 // No reasoning block found
@@ -886,6 +1007,7 @@ function registerReasoningAppEvents() {
886 // If reasoning was found, add it to the message1007 // If reasoning was found, add it to the message
887 if (parsedReasoning.reasoning) {1008 if (parsedReasoning.reasoning) {
888 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);1009 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
1010 message.extra.reasoning_type = ReasoningType.Parsed;
889 }1011 }
8901012
891 // Update the message text if it was changed1013 // Update the message text if it was changed
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -34,6 +34,7 @@ export const enumIcons = {
34 preset: '⚙️',34 preset: '⚙️',
35 file: '📄',35 file: '📄',
36 message: '💬',36 message: '💬',
37 reasoning: '💡',
37 voice: '🎤',38 voice: '🎤',
38 server: '🖥️',39 server: '🖥️',
39 popup: '🗔',40 popup: '🗔',
public/scripts/textgen-settings.js+2 -2
@@ -311,7 +311,7 @@ export function validateTextGenUrl() {
311 const formattedUrl = formatTextGenURL(url);311 const formattedUrl = formatTextGenURL(url);
312312
313 if (!formattedUrl) {313 if (!formattedUrl) {
314 toastr.error('Enter a valid API URL', 'Text Completion API');314 toastr.error(t`Enter a valid API URL`, 'Text Completion API');
315 return;315 return;
316 }316 }
317317
@@ -1187,7 +1187,7 @@ export function getTextGenModel() {
1187 return settings.aphrodite_model;1187 return settings.aphrodite_model;
1188 case OLLAMA:1188 case OLLAMA:
1189 if (!settings.ollama_model) {1189 if (!settings.ollama_model) {
1190 toastr.error('No Ollama model selected.', 'Text Completion API');1190 toastr.error(t`No Ollama model selected.`, 'Text Completion API');
1191 throw new Error('No Ollama model selected');1191 throw new Error('No Ollama model selected');
1192 }1192 }
1193 return settings.ollama_model;1193 return settings.ollama_model;
public/scripts/utils.js+14 -1
@@ -8,7 +8,7 @@ import {
8import { getContext } from './extensions.js';8import { getContext } from './extensions.js';
9import { characters, getRequestHeaders, this_chid } from '../script.js';9import { characters, getRequestHeaders, this_chid } from '../script.js';
10import { isMobile } from './RossAscends-mods.js';10import { isMobile } from './RossAscends-mods.js';
11import { collapseNewlines } from './power-user.js';11import { collapseNewlines, power_user } from './power-user.js';
12import { debounce_timeout } from './constants.js';12import { debounce_timeout } from './constants.js';
13import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';13import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
14import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';14import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -677,6 +677,19 @@ export function sortByCssOrder(a, b) {
677}677}
678678
679/**679/**
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
685export 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/**
680 * Trims a string to the end of a nearest sentence.693 * Trims a string to the end of a nearest sentence.
681 * @param {string} input The string to trim.694 * @param {string} input The string to trim.
682 * @returns {string} The trimmed string.695 * @returns {string} The trimmed string.
src/endpoints/vectors.js+11 -11
@@ -132,35 +132,35 @@ function getSourceSettings(source, request) {
132 switch (source) {132 switch (source) {
133 case 'togetherai':133 case 'togetherai':
134 return {134 return {
135 model: String(request.headers['x-togetherai-model']),135 model: String(request.body.model),
136 };136 };
137 case 'openai':137 case 'openai':
138 return {138 return {
139 model: String(request.headers['x-openai-model']),139 model: String(request.body.model),
140 };140 };
141 case 'cohere':141 case 'cohere':
142 return {142 return {
143 model: String(request.headers['x-cohere-model']),143 model: String(request.body.model),
144 };144 };
145 case 'llamacpp':145 case 'llamacpp':
146 return {146 return {
147 apiUrl: String(request.headers['x-llamacpp-url']),147 apiUrl: String(request.body.apiUrl),
148 };148 };
149 case 'vllm':149 case 'vllm':
150 return {150 return {
151 apiUrl: String(request.headers['x-vllm-url']),151 apiUrl: String(request.body.apiUrl),
152 model: String(request.headers['x-vllm-model']),152 model: String(request.body.model),
153 };153 };
154 case 'ollama':154 case 'ollama':
155 return {155 return {
156 apiUrl: String(request.headers['x-ollama-url']),156 apiUrl: String(request.body.apiUrl),
157 model: String(request.headers['x-ollama-model']),157 model: String(request.body.model),
158 keep: Boolean(request.headers['x-ollama-keep']),158 keep: Boolean(request.body.keep),
159 };159 };
160 case 'extras':160 case 'extras':
161 return {161 return {
162 extrasUrl: String(request.headers['x-extras-url']),162 extrasUrl: String(request.body.extrasUrl),
163 extrasKey: String(request.headers['x-extras-key']),163 extrasKey: String(request.body.extrasKey),
164 };164 };
165 case 'transformers':165 case 'transformers':
166 return {166 return {