Merge branch 'staging' into support-multiple-expressions

25ffc4ac4ae9ac54e877c25266aeec1e4bb64d73

Wolfsblvt <wolfsblvt@gmail.com>

38 files changed, +1236 -271Showing whitespace changes
.github/readme.md+1 -1
@@ -274,7 +274,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
2741. Open your Command Line2741. Open your Command Line
2752. Run the following command2752. Run the following command
276276
277`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`277`docker run --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
278278
279> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.279> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.
280280
public/css/toggle-dependent.css+5 -0
@@ -472,6 +472,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
472 display: none;472 display: none;
473}473}
474474
475label[for="trim_spaces"]:not(:has(input:checked)) small {
476 color: var(--warning);
477 opacity: 1;
478}
479
475#claude_function_prefill_warning {480#claude_function_prefill_warning {
476 display: none;481 display: none;
477 color: red;482 color: red;
public/index.html+70 -28
@@ -1587,6 +1587,10 @@
1587 <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />1587 <input type="checkbox" id="skip_special_tokens_textgenerationwebui" />
1588 <small data-i18n="Skip Special Tokens">Skip Special Tokens</small>1588 <small data-i18n="Skip Special Tokens">Skip Special Tokens</small>
1589 </label>1589 </label>
1590 <label data-tg-type="openrouter" class="checkbox_label flexGrow flexShrink" for="include_reasoning_textgenerationwebui">
1591 <input type="checkbox" id="include_reasoning_textgenerationwebui" />
1592 <small data-i18n="Request Model Reasoning">Request Model Reasoning</small>
1593 </label>
1590 <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">1594 <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
1591 <input type="checkbox" id="temperature_last_textgenerationwebui" />1595 <input type="checkbox" id="temperature_last_textgenerationwebui" />
1592 <label>1596 <label>
@@ -1977,17 +1981,17 @@
1977 </span>1981 </span>
1978 </div>1982 </div>
1979 </div>1983 </div>
1980 <div class="range-block" data-source="makersuite,deepseek">1984 <div class="range-block" data-source="makersuite,deepseek,openrouter">
1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">1985 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
1982 <input id="openai_show_thoughts" type="checkbox" />1986 <input id="openai_show_thoughts" type="checkbox" />
1983 <span>1987 <span>
1984 <span data-i18n="Show model thoughts">Show model thoughts</span>1988 <span data-i18n="Request model reasoning">Request model reasoning</span>
1985 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>1989 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
1986 </span>1990 </span>
1987 </label>1991 </label>
1988 <div class="toggle-description justifyLeft marginBot5">1992 <div class="toggle-description justifyLeft marginBot5">
1989 <span data-i18n="Display the model's internal thoughts in the response.">1993 <span data-i18n="Allows the model to return its thinking process.">
1990 Display the model's internal thoughts in the response.1994 Allows the model to return its thinking process.
1991 </span>1995 </span>
1992 </div>1996 </div>
1993 </div>1997 </div>
@@ -3227,32 +3231,19 @@
3227 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>3231 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
3228 <select id="model_perplexity_select">3232 <select id="model_perplexity_select">
3229 <optgroup label="Perplexity Sonar Models">3233 <optgroup label="Perplexity Sonar Models">
3234 <option value="sonar">sonar</option>
3235 <option value="sonar-pro">sonar-pro</option>
3236 <option value="sonar-reasoning">sonar-reasoning</option>
3237 </optgroup>
3238 <optgroup label="Deprecated Models">
3239 <!-- These are scheduled for deprecation after 2/22/2025 -->
3230 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>3240 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
3231 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>3241 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
3232 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>3242 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
3233 </optgroup>3243 <!-- These are not listed on the site anymore -->
3234 <optgroup label="Perplexity Chat Models">
3235 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>3244 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
3236 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>3245 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
3237 </optgroup>3246 </optgroup>
3238 <optgroup label="Open-Source Models">
3239 <option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option>
3240 <option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option>
3241 </optgroup>
3242 <optgroup label="Deprecated Models">
3243 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
3244 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
3245 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
3246 <option value="llama-3-sonar-large-32k-online">llama-3-sonar-large-32k-online</option>
3247 <option value="sonar-small-chat">sonar-small-chat</option>
3248 <option value="sonar-small-online">sonar-small-online</option>
3249 <option value="sonar-medium-chat">sonar-medium-chat</option>
3250 <option value="sonar-medium-online">sonar-medium-online</option>
3251 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
3252 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
3253 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
3254 <option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
3255 </optgroup>
3256 </select>3247 </select>
3257 </div>3248 </div>
3258 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">3249 <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@@ -3800,6 +3791,45 @@
3800 </div>3791 </div>
3801 </div>3792 </div>
3802 <div>3793 <div>
3794 <h4 class="standoutHeader">
3795 <span data-i18n="Reasoning">Reasoning</span>
3796 </h4>
3797 <div>
3798 <label class="checkbox_label" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse">
3799 <input id="reasoning_auto_parse" type="checkbox" />
3800 <small data-i18n="Auto-Parse Reasoning">
3801 Auto-Parse Reasoning
3802 </small>
3803 </label>
3804 <label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
3805 <input id="reasoning_add_to_prompts" type="checkbox" />
3806 <small data-i18n="Add Reasoning to Prompts">
3807 Add Reasoning to Prompts
3808 </small>
3809 </label>
3810 <div class="flex-container">
3811 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
3812 <small data-i18n="Prefix">Prefix</small>
3813 <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>
3814 </div>
3815 <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
3816 <small data-i18n="Suffix">Suffix</small>
3817 <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>
3818 </div>
3819 </div>
3820 <div class="flex-container">
3821 <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
3822 <small data-i18n="Separator">Separator</small>
3823 <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>
3824 </div>
3825 <div class="flex1" title="Maximum number of reasoning blocks to be added per prompt, counting from the last message." data-i18n="[title]reasoning_max_additions">
3826 <small data-i18n="Max Additions">Max Additions</small>
3827 <input id="reasoning_max_additions" class="text_pole textarea_compact" type="number" min="0" max="999"></textarea>
3828 </div>
3829 </div>
3830 </div>
3831 </div>
3832 <div>
3803 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>3833 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
3804 <div>3834 <div>
3805 <small>3835 <small>
@@ -6221,14 +6251,26 @@
6221 <div class="mes_edit_buttons">6251 <div class="mes_edit_buttons">
6222 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>6252 <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
6223 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>6253 <div class="mes_edit_copy menu_button fa-solid fa-copy" title="Copy this message" data-i18n="[title]Copy this message"></div>
6224 <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message">6254 <div class="mes_edit_add_reasoning menu_button fa-solid fa-lightbulb" title="Add a reasoning block" data-i18n="[title]Add a reasoning block"></div>
6225 </div>6255 <div class="mes_edit_delete menu_button fa-solid fa-trash-can" title="Delete this message" data-i18n="[title]Delete this message"></div>
6226 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>6256 <div class="mes_edit_up menu_button fa-solid fa-chevron-up " title="Move message up" data-i18n="[title]Move message up"></div>
6227 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down">6257 <div class="mes_edit_down menu_button fa-solid fa-chevron-down" title="Move message down" data-i18n="[title]Move message down"></div>
6228 </div>
6229 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>6258 <div class="mes_edit_cancel menu_button fa-solid fa-xmark" title="Cancel" data-i18n="[title]Cancel"></div>
6230 </div>6259 </div>
6231 </div>6260 </div>
6261 <details class="mes_reasoning_details">
6262 <summary class="mes_reasoning_summary">
6263 <span data-i18n="Reasoning">Reasoning</span>
6264 <div class="mes_reasoning_actions">
6265 <div class="mes_reasoning_edit_done mes_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirmedit"></div>
6266 <div class="mes_reasoning_edit_cancel mes_button fa-solid fa-xmark" title="Cancel edit" data-i18n="[title]Cancel edit"></div>
6267 <div class="mes_reasoning_edit mes_button fa-solid fa-pencil" title="Edit reasoning" data-i18n="[title]Edit reasoning"></div>
6268 <div class="mes_reasoning_copy mes_button fa-solid fa-copy" title="Copy reasoning" data-i18n="[title]Copy reasoning"></div>
6269 <div class="mes_reasoning_delete mes_button fa-solid fa-trash-can" title="Remove reasoning" data-i18n="[title]Remove reasoning"></div>
6270 </div>
6271 </summary>
6272 <div class="mes_reasoning"></div>
6273 </details>
6232 <div class="mes_text"></div>6274 <div class="mes_text"></div>
6233 <div class="mes_img_container">6275 <div class="mes_img_container">
6234 <div class="mes_img_controls">6276 <div class="mes_img_controls">
public/locales/fr-fr.json+2 -2
@@ -1385,8 +1385,8 @@
1385 "enable_functions_desc_1": "Autorise l'utilisation",1385 "enable_functions_desc_1": "Autorise l'utilisation",
1386 "enable_functions_desc_2": "outils de fonction",1386 "enable_functions_desc_2": "outils de fonction",
1387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",1387 "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
1388 "Show model thoughts": "Afficher les pensées du modèle",1388 "Request model reasoning": "Demander les pensées du modèle",
1389 "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",1389 "Allows the model to return its thinking process.": "Permet au modèle de retourner son processus de réflexion.",
1390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",1390 "Confirm token parsing with": "Confirmer l'analyse des tokens avec",
1391 "openai_logit_bias_no_items": "Aucun élément",1391 "openai_logit_bias_no_items": "Aucun élément",
1392 "api_no_connection": "Pas de connection...",1392 "api_no_connection": "Pas de connection...",
public/locales/zh-cn.json+2 -2
@@ -266,8 +266,8 @@
266 "Use system prompt": "使用系统提示词",266 "Use system prompt": "使用系统提示词",
267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",267 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
268 "Merges_all_system_messages_desc_2": "字段发送。",268 "Merges_all_system_messages_desc_2": "字段发送。",
269 "Show model thoughts": "展示思维链",269 "Request model reasoning": "请求思维链",
270 "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",270 "Allows the model to return its thinking process.": "允许模型返回其思维过程。",
271 "Assistant Prefill": "AI预填",271 "Assistant Prefill": "AI预填",
272 "Expand the editor": "展开编辑器",272 "Expand the editor": "展开编辑器",
273 "Start Claude's answer with...": "以如下内容开始Claude的回答...",273 "Start Claude's answer with...": "以如下内容开始Claude的回答...",
public/locales/zh-tw.json+2 -2
@@ -2356,8 +2356,8 @@
2356 "Forbid": "禁止",2356 "Forbid": "禁止",
2357 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",2357 "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
2358 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",2358 "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
2359 "Show model thoughts": "顯示模型思維鏈",2359 "Request model reasoning": "請求模型思維鏈",
2360 "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",2360 "Allows the model to return its thinking process.": "讓模型回傳其思考過程。",
2361 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",2361 "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
2362 "Model ID (optional)": "模型 ID(可選)",2362 "Model ID (optional)": "模型 ID(可選)",
2363 "DeepSeek API Key": "DeepSeek API 金鑰",2363 "DeepSeek API Key": "DeepSeek API 金鑰",
public/script.js+186 -89
@@ -95,6 +95,7 @@ import {
95 resetMovableStyles,95 resetMovableStyles,
96 forceCharacterEditorTokenize,96 forceCharacterEditorTokenize,
97 applyPowerUserSettings,97 applyPowerUserSettings,
98 generatedTextFiltered,
98} from './scripts/power-user.js';99} from './scripts/power-user.js';
99100
100import {101import {
@@ -169,6 +170,7 @@ import {
169 toggleDrawer,170 toggleDrawer,
170 isElementInViewport,171 isElementInViewport,
171 copyText,172 copyText,
173 escapeHtml,
172} from './scripts/utils.js';174} from './scripts/utils.js';
173import { debounce_timeout } from './scripts/constants.js';175import { debounce_timeout } from './scripts/constants.js';
174176
@@ -267,6 +269,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
267import { initBulkEdit } from './scripts/bulk-edit.js';269import { initBulkEdit } from './scripts/bulk-edit.js';
268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
269import { getContext } from './scripts/st-context.js';271import { getContext } from './scripts/st-context.js';
272import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
270273
271// API OBJECT FOR EXTERNAL WIRING274// API OBJECT FOR EXTERNAL WIRING
272globalThis.SillyTavern = {275globalThis.SillyTavern = {
@@ -982,6 +985,7 @@ async function firstLoadInit() {
982 initServerHistory();985 initServerHistory();
983 initSettingsSearch();986 initSettingsSearch();
984 initBulkEdit();987 initBulkEdit();
988 initReasoning();
985 await initScrapers();989 await initScrapers();
986 doDailyExtensionUpdatesCheck();990 doDailyExtensionUpdatesCheck();
987 await hideLoader();991 await hideLoader();
@@ -1991,14 +1995,15 @@ export async function sendTextareaMessage() {
1991 * @param {boolean} isUser If the message was sent by the user1995 * @param {boolean} isUser If the message was sent by the user
1992 * @param {number} messageId Message index in chat array1996 * @param {number} messageId Message index in chat array
1993 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides1997 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
1998 * @param {boolean} [isReasoning] If the message is reasoning output
1994 * @returns {string} HTML string1999 * @returns {string} HTML string
1995 */2000 */
1996export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {2001export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
1997 if (!mes) {2002 if (!mes) {
1998 return '';2003 return '';
1999 }2004 }
20002005
2001 if (Number(messageId) === 0 && !isSystem && !isUser) {2006 if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) {
2002 const mesBeforeReplace = mes;2007 const mesBeforeReplace = mes;
2003 const chatMessage = chat[messageId];2008 const chatMessage = chat[messageId];
2004 mes = substituteParams(mes, undefined, ch_name);2009 mes = substituteParams(mes, undefined, ch_name);
@@ -2027,6 +2032,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2027 if (!isSystem) {2032 if (!isSystem) {
2028 function getRegexPlacement() {2033 function getRegexPlacement() {
2029 try {2034 try {
2035 if (isReasoning) {
2036 return regex_placement.REASONING;
2037 }
2030 if (isUser) {2038 if (isUser) {
2031 return regex_placement.USER_INPUT;2039 return regex_placement.USER_INPUT;
2032 } else if (chat[messageId]?.extra?.type === 'narrator') {2040 } else if (chat[messageId]?.extra?.type === 'narrator') {
@@ -2060,6 +2068,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2060 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');2068 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
2061 }2069 }
20622070
2071 // Make sure reasoning strings are always shown, even if they include "<" or ">"
2072 [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
2073 if (!reasoningString || !reasoningString.trim().length) {
2074 return;
2075 }
2076 // Only replace the first occurrence of the reasoning string
2077 if (mes.includes(reasoningString)) {
2078 mes = mes.replace(reasoningString, escapeHtml(reasoningString));
2079 }
2080 });
2081
2063 if (!isSystem) {2082 if (!isSystem) {
2064 // Save double quotes in tags as a special character to prevent them from being encoded2083 // Save double quotes in tags as a special character to prevent them from being encoded
2065 if (!power_user.encode_tags) {2084 if (!power_user.encode_tags) {
@@ -2200,6 +2219,7 @@ function getMessageFromTemplate({
2200 isUser,2219 isUser,
2201 avatarImg,2220 avatarImg,
2202 bias,2221 bias,
2222 reasoning,
2203 isSystem,2223 isSystem,
2204 title,2224 title,
2205 timerValue,2225 timerValue,
@@ -2224,6 +2244,7 @@ function getMessageFromTemplate({
2224 mes.find('.avatar img').attr('src', avatarImg);2244 mes.find('.avatar img').attr('src', avatarImg);
2225 mes.find('.ch_name .name_text').text(characterName);2245 mes.find('.ch_name .name_text').text(characterName);
2226 mes.find('.mes_bias').html(bias);2246 mes.find('.mes_bias').html(bias);
2247 mes.find('.mes_reasoning').html(reasoning);
2227 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);2248 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
2228 mes.find('.mesIDDisplay').text(`#${mesId}`);2249 mes.find('.mesIDDisplay').text(`#${mesId}`);
2229 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);2250 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2238,10 +2259,16 @@ function getMessageFromTemplate({
2238 return mes;2259 return mes;
2239}2260}
22402261
2262/**
2263 * Re-renders a message block with updated content.
2264 * @param {number} messageId Message ID
2265 * @param {object} message Message object
2266 */
2241export function updateMessageBlock(messageId, message) {2267export function updateMessageBlock(messageId, message) {
2242 const messageElement = $(`#chat [mesid="${messageId}"]`);2268 const messageElement = $(`#chat [mesid="${messageId}"]`);
2243 const text = message?.extra?.display_text ?? message.mes;2269 const text = message?.extra?.display_text ?? message.mes;
2244 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));2270 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2271 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
2245 addCopyToCodeBlocks(messageElement);2272 addCopyToCodeBlocks(messageElement);
2246 appendMediaToMessage(message, messageElement);2273 appendMediaToMessage(message, messageElement);
2247}2274}
@@ -2398,8 +2425,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2398 mes.is_user,2425 mes.is_user,
2399 chat.indexOf(mes),2426 chat.indexOf(mes),
2400 sanitizerOverrides,2427 sanitizerOverrides,
2428 false,
2401 );2429 );
2402 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);2430 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2431 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
2403 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2432 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24042433
2405 let params = {2434 let params = {
@@ -2409,6 +2438,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2409 isUser: mes.is_user,2438 isUser: mes.is_user,
2410 avatarImg: avatarImg,2439 avatarImg: avatarImg,
2411 bias: bias,2440 bias: bias,
2441 reasoning: reasoning,
2412 isSystem: isSystem,2442 isSystem: isSystem,
2413 title: title,2443 title: title,
2414 bookmarkLink: bookmarkLink,2444 bookmarkLink: bookmarkLink,
@@ -2468,6 +2498,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2468 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);2498 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
2469 swipeMessage.attr('swipeid', params.swipeId);2499 swipeMessage.attr('swipeid', params.swipeId);
2470 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2500 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2501 swipeMessage.find('.mes_reasoning').html(reasoning);
2471 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);2502 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2472 appendMediaToMessage(mes, swipeMessage);2503 appendMediaToMessage(mes, swipeMessage);
2473 if (power_user.timestamp_model_icon && params.extra?.api) {2504 if (power_user.timestamp_model_icon && params.extra?.api) {
@@ -3044,7 +3075,7 @@ export function isStreamingEnabled() {
3044 (main_api == 'openai' &&3075 (main_api == 'openai' &&
3045 oai_settings.stream_openai &&3076 oai_settings.stream_openai &&
3046 !noStreamSources.includes(oai_settings.chat_completion_source) &&3077 !noStreamSources.includes(oai_settings.chat_completion_source) &&
3047 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && oai_settings.openai_model.startsWith('o1-')) &&3078 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) &&
3048 !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))3079 !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))
3049 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)3080 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
3050 || (main_api == 'novel' && nai_settings.streaming_novel)3081 || (main_api == 'novel' && nai_settings.streaming_novel)
@@ -3078,6 +3109,7 @@ class StreamingProcessor {
3078 this.messageTextDom = null;3109 this.messageTextDom = null;
3079 this.messageTimerDom = null;3110 this.messageTimerDom = null;
3080 this.messageTokenCounterDom = null;3111 this.messageTokenCounterDom = null;
3112 this.messageReasoningDom = null;
3081 /** @type {HTMLTextAreaElement} */3113 /** @type {HTMLTextAreaElement} */
3082 this.sendTextarea = document.querySelector('#send_textarea');3114 this.sendTextarea = document.querySelector('#send_textarea');
3083 this.type = type;3115 this.type = type;
@@ -3093,6 +3125,7 @@ class StreamingProcessor {
3093 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */3125 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
3094 this.messageLogprobs = [];3126 this.messageLogprobs = [];
3095 this.toolCalls = [];3127 this.toolCalls = [];
3128 this.reasoning = '';
3096 }3129 }
30973130
3098 #checkDomElements(messageId) {3131 #checkDomElements(messageId) {
@@ -3101,6 +3134,7 @@ class StreamingProcessor {
3101 this.messageTextDom = this.messageDom?.querySelector('.mes_text');3134 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
3102 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');3135 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
3103 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');3136 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3137 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
3104 }3138 }
3105 }3139 }
31063140
@@ -3137,7 +3171,7 @@ class StreamingProcessor {
3137 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3171 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3138 }3172 }
3139 else {3173 else {
3140 await saveReply(this.type, text, true);3174 await saveReply(this.type, text, true, '', [], '');
3141 messageId = chat.length - 1;3175 messageId = chat.length - 1;
3142 this.#checkDomElements(messageId);3176 this.#checkDomElements(messageId);
3143 this.showMessageButtons(messageId);3177 this.showMessageButtons(messageId);
@@ -3178,18 +3212,27 @@ class StreamingProcessor {
3178 this.#checkDomElements(messageId);3212 this.#checkDomElements(messageId);
3179 this.#updateMessageBlockVisibility();3213 this.#updateMessageBlockVisibility();
3180 const currentTime = new Date();3214 const currentTime = new Date();
3181 // Don't waste time calculating token count for streaming
3182 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
3183 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
3184 chat[messageId]['mes'] = processedText;3215 chat[messageId]['mes'] = processedText;
3185 chat[messageId]['gen_started'] = this.timeStarted;3216 chat[messageId]['gen_started'] = this.timeStarted;
3186 chat[messageId]['gen_finished'] = currentTime;3217 chat[messageId]['gen_finished'] = currentTime;
31873218
3188 if (currentTokenCount) {
3189 if (!chat[messageId]['extra']) {3219 if (!chat[messageId]['extra']) {
3190 chat[messageId]['extra'] = {};3220 chat[messageId]['extra'] = {};
3191 }3221 }
31923222
3223 if (this.reasoning) {
3224 chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
3225 if (this.messageReasoningDom instanceof HTMLElement) {
3226 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
3227 this.messageReasoningDom.innerHTML = formattedReasoning;
3228 }
3229 }
3230
3231 // Don't waste time calculating token count for streaming
3232 const tokenCountText = (this.reasoning || '') + processedText;
3233 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3234
3235 if (currentTokenCount) {
3193 chat[messageId]['extra']['token_count'] = currentTokenCount;3236 chat[messageId]['extra']['token_count'] = currentTokenCount;
3194 if (this.messageTokenCounterDom instanceof HTMLElement) {3237 if (this.messageTokenCounterDom instanceof HTMLElement) {
3195 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;3238 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
@@ -3207,14 +3250,19 @@ class StreamingProcessor {
3207 chat[messageId].is_system,3250 chat[messageId].is_system,
3208 chat[messageId].is_user,3251 chat[messageId].is_user,
3209 messageId,3252 messageId,
3253 {},
3254 false,
3210 );3255 );
3211 if (this.messageTextDom instanceof HTMLElement) {3256 if (this.messageTextDom instanceof HTMLElement) {
3212 this.messageTextDom.innerHTML = formattedText;3257 this.messageTextDom.innerHTML = formattedText;
3213 }3258 }
3259
3260 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
3214 if (this.messageTimerDom instanceof HTMLElement) {3261 if (this.messageTimerDom instanceof HTMLElement) {
3215 this.messageTimerDom.textContent = timePassed.timerValue;3262 this.messageTimerDom.textContent = timePassed.timerValue;
3216 this.messageTimerDom.title = timePassed.timerTitle;3263 this.messageTimerDom.title = timePassed.timerTitle;
3217 }3264 }
3265
3218 this.setFirstSwipe(messageId);3266 this.setFirstSwipe(messageId);
3219 }3267 }
32203268
@@ -3255,39 +3303,11 @@ class StreamingProcessor {
3255 unblockGeneration();3303 unblockGeneration();
3256 generatedPromptCache = '';3304 generatedPromptCache = '';
32573305
3258 //console.log("Generated text size:", text.length, text)
3259
3260 const isAborted = this.abortController.signal.aborted;3306 const isAborted = this.abortController.signal.aborted;
3261 if (power_user.auto_swipe && !isAborted) {3307 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
3262 function containsBlacklistedWords(str, blacklist, threshold) {3308 return swipe_right();
3263 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
3264 const matches = str.match(regex) || [];
3265 return matches.length >= threshold;
3266 }
3267
3268 const generatedTextFiltered = (text) => {
3269 if (text) {
3270 if (power_user.auto_swipe_minimum_length) {
3271 if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
3272 console.log('Generated text size too small');
3273 return true;
3274 }
3275 }3309 }
3276 if (power_user.auto_swipe_blacklist_threshold) {
3277 if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
3278 console.log('Generated text has blacklisted words');
3279 return true;
3280 }
3281 }
3282 }
3283 return false;
3284 };
32853310
3286 if (generatedTextFiltered(text)) {
3287 swipe_right();
3288 return;
3289 }
3290 }
3291 playMessageSound();3311 playMessageSound();
3292 }3312 }
32933313
@@ -3321,7 +3341,7 @@ class StreamingProcessor {
3321 }3341 }
33223342
3323 /**3343 /**
3324 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}3344 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>}
3325 */3345 */
3326 *nullStreamingGeneration() {3346 *nullStreamingGeneration() {
3327 throw new Error('Generation function for streaming is not hooked up');3347 throw new Error('Generation function for streaming is not hooked up');
@@ -3343,9 +3363,9 @@ class StreamingProcessor {
3343 try {3363 try {
3344 const sw = new Stopwatch(1000 / power_user.streaming_fps);3364 const sw = new Stopwatch(1000 / power_user.streaming_fps);
3345 const timestamps = [];3365 const timestamps = [];
3346 for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {3366 for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
3347 timestamps.push(Date.now());3367 timestamps.push(Date.now());
3348 if (this.isStopped) {3368 if (this.isStopped || this.abortController.signal.aborted) {
3349 return;3369 return;
3350 }3370 }
33513371
@@ -3355,6 +3375,7 @@ class StreamingProcessor {
3355 if (logprobs) {3375 if (logprobs) {
3356 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3376 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3357 }3377 }
3378 this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
3358 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3379 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3359 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));3380 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
3360 }3381 }
@@ -3840,6 +3861,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3840 };3861 };
3841 }));3862 }));
38423863
3864 const reasoning = new PromptReasoning();
3865 for (let i = coreChat.length - 1; i >= 0; i--) {
3866 const depth = coreChat.length - i - 1;
3867 const isPrefix = isContinue && i === coreChat.length - 1;
3868 coreChat[i] = {
3869 ...coreChat[i],
3870 mes: reasoning.addToMessage(
3871 coreChat[i].mes,
3872 getRegexedString(
3873 String(coreChat[i].extra?.reasoning ?? ''),
3874 regex_placement.REASONING,
3875 { isPrompt: true, depth: depth },
3876 ),
3877 isPrefix,
3878 ),
3879 };
3880 if (reasoning.isLimitReached()) {
3881 break;
3882 }
3883 }
3884
3843 // Determine token limit3885 // Determine token limit
3844 let this_max_context = getMaxContextSize();3886 let this_max_context = getMaxContextSize();
38453887
@@ -4742,11 +4784,17 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4742 //const getData = await response.json();4784 //const getData = await response.json();
4743 let getMessage = extractMessageFromData(data);4785 let getMessage = extractMessageFromData(data);
4744 let title = extractTitleFromData(data);4786 let title = extractTitleFromData(data);
4787 let reasoning = extractReasoningFromData(data);
4745 kobold_horde_model = title;4788 kobold_horde_model = title;
47464789
4747 const swipes = extractMultiSwipes(data, type);4790 const swipes = extractMultiSwipes(data, type);
47484791
4749 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);4792 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
4793 reasoning = getRegexedString(reasoning, regex_placement.REASONING);
4794
4795 if (power_user.trim_spaces) {
4796 reasoning = reasoning.trim();
4797 }
47504798
4751 if (isContinue) {4799 if (isContinue) {
4752 getMessage = continue_mag + getMessage;4800 getMessage = continue_mag + getMessage;
@@ -4768,10 +4816,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4768 else {4816 else {
4769 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.4817 // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
4770 if (originalType !== 'continue') {4818 if (originalType !== 'continue') {
4771 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes));4819 ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning));
4772 }4820 }
4773 else {4821 else {
4774 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes));4822 ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning));
4775 }4823 }
47764824
4777 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.4825 // This relies on `saveReply` having been called to add the message to the chat, so it must be last.
@@ -4805,32 +4853,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4805 }4853 }
48064854
4807 const isAborted = abortController && abortController.signal.aborted;4855 const isAborted = abortController && abortController.signal.aborted;
4808 if (power_user.auto_swipe && !isAborted) {4856 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
4809 console.debug('checking for autoswipeblacklist on non-streaming message');
4810 function containsBlacklistedWords(getMessage, blacklist, threshold) {
4811 console.debug('checking blacklisted words');
4812 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
4813 const matches = getMessage.match(regex) || [];
4814 return matches.length >= threshold;
4815 }
4816
4817 const generatedTextFiltered = (getMessage) => {
4818 if (power_user.auto_swipe_blacklist_threshold) {
4819 if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
4820 console.debug('Generated text has blacklisted words');
4821 return true;
4822 }
4823 }
4824
4825 return false;
4826 };
4827 if (generatedTextFiltered(getMessage)) {
4828 console.debug('swiping right automatically');
4829 is_send_press = false;4857 is_send_press = false;
4830 swipe_right();4858 return swipe_right();
4831 // TODO: do we want to resolve after an auto-swipe?
4832 return;
4833 }
4834 }4859 }
48354860
4836 console.debug('/api/chats/save called by /Generate');4861 console.debug('/api/chats/save called by /Generate');
@@ -5659,7 +5684,6 @@ function extractMessageFromData(data) {
5659 return data;5684 return data;
5660 }5685 }
56615686
5662 function getTextContext() {
5663 switch (main_api) {5687 switch (main_api) {
5664 case 'kobold':5688 case 'kobold':
5665 return data.results[0].text;5689 return data.results[0].text;
@@ -5676,14 +5700,35 @@ function extractMessageFromData(data) {
5676 }5700 }
5677}5701}
56785702
5679 const content = getTextContext();5703/**
5704 * Extracts the reasoning from the response data.
5705 * @param {object} data Response data
5706 * @returns {string} Extracted reasoning
5707 */
5708function extractReasoningFromData(data) {
5709 switch (main_api) {
5710 case 'textgenerationwebui':
5711 switch (textgen_settings.type) {
5712 case textgen_types.OPENROUTER:
5713 return data?.choices?.[0]?.reasoning ?? '';
5714 }
5715 break;
56805716
5681 if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) {5717 case 'openai':
5682 const thoughts = data?.choices?.[0]?.message?.reasoning_content ?? '';5718 if (!oai_settings.show_thoughts) break;
5683 return [thoughts, content].filter(x => x).join('\n\n');5719
5720 switch (oai_settings.chat_completion_source) {
5721 case chat_completion_sources.DEEPSEEK:
5722 return data?.choices?.[0]?.message?.reasoning_content ?? '';
5723 case chat_completion_sources.OPENROUTER:
5724 return data?.choices?.[0]?.message?.reasoning ?? '';
5725 case chat_completion_sources.MAKERSUITE:
5726 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5727 }
5728 break;
5684 }5729 }
56855730
5686 return content;5731 return '';
5687}5732}
56885733
5689/**5734/**
@@ -5866,7 +5911,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5866 return getMessage;5911 return getMessage;
5867}5912}
58685913
5869export async function saveReply(type, getMessage, fromStreaming, title, swipes) {5914export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) {
5870 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||5915 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
5871 chat[chat.length - 1]['is_user'])) {5916 chat[chat.length - 1]['is_user'])) {
5872 type = 'normal';5917 type = 'normal';
@@ -5891,8 +5936,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5891 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5936 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5892 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5937 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5893 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5938 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5939 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5894 if (power_user.message_token_count_enabled) {5940 if (power_user.message_token_count_enabled) {
5895 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5941 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5942 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5896 }5943 }
5897 const chat_id = (chat.length - 1);5944 const chat_id = (chat.length - 1);
5898 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5945 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5911,8 +5958,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5911 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5958 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5912 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5959 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5913 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5960 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5961 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5914 if (power_user.message_token_count_enabled) {5962 if (power_user.message_token_count_enabled) {
5915 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5963 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5964 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5916 }5965 }
5917 const chat_id = (chat.length - 1);5966 const chat_id = (chat.length - 1);
5918 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5967 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5928,8 +5977,10 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5928 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5977 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5929 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5978 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5930 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5979 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5980 chat[chat.length - 1]['extra']['reasoning'] += reasoning;
5931 if (power_user.message_token_count_enabled) {5981 if (power_user.message_token_count_enabled) {
5932 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);5982 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
5983 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5933 }5984 }
5934 const chat_id = (chat.length - 1);5985 const chat_id = (chat.length - 1);
5935 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);5986 await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
@@ -5945,6 +5996,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5945 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();5996 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
5946 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();5997 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
5947 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();5998 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
5999 chat[chat.length - 1]['extra']['reasoning'] = reasoning;
5948 if (power_user.trim_spaces) {6000 if (power_user.trim_spaces) {
5949 getMessage = getMessage.trim();6001 getMessage = getMessage.trim();
5950 }6002 }
@@ -5954,7 +6006,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
5954 chat[chat.length - 1]['gen_finished'] = generationFinished;6006 chat[chat.length - 1]['gen_finished'] = generationFinished;
59556007
5956 if (power_user.message_token_count_enabled) {6008 if (power_user.message_token_count_enabled) {
5957 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0);6009 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];
6010 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
5958 }6011 }
59596012
5960 if (selected_group) {6013 if (selected_group) {
@@ -6019,6 +6072,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes)
6019 return { type, getMessage };6072 return { type, getMessage };
6020}6073}
60216074
6075export function syncCurrentSwipeInfoExtras() {
6076 if (!chat.length) {
6077 return;
6078 }
6079 const currentMessage = chat[chat.length - 1];
6080 if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
6081 const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
6082 if (swipeInfo && typeof swipeInfo === 'object') {
6083 swipeInfo.extra = structuredClone(currentMessage.extra);
6084 }
6085 }
6086}
6087
6022function saveImageToMessage(img, mes) {6088function saveImageToMessage(img, mes) {
6023 if (mes && img.image) {6089 if (mes && img.image) {
6024 if (!mes.extra || typeof mes.extra !== 'object') {6090 if (!mes.extra || typeof mes.extra !== 'object') {
@@ -7122,9 +7188,11 @@ function messageEditAuto(div) {
7122 mes.is_system,7188 mes.is_system,
7123 mes.is_user,7189 mes.is_user,
7124 this_edit_mes_id,7190 this_edit_mes_id,
7191 {},
7192 false,
7125 ));7193 ));
7126 mesBlock.find('.mes_bias').empty();7194 mesBlock.find('.mes_bias').empty();
7127 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));7195 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
7128 saveChatDebounced();7196 saveChatDebounced();
7129}7197}
71307198
@@ -7146,10 +7214,12 @@ async function messageEditDone(div) {
7146 mes.is_system,7214 mes.is_system,
7147 mes.is_user,7215 mes.is_user,
7148 this_edit_mes_id,7216 this_edit_mes_id,
7217 {},
7218 false,
7149 ),7219 ),
7150 );7220 );
7151 mesBlock.find('.mes_bias').empty();7221 mesBlock.find('.mes_bias').empty();
7152 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));7222 mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
7153 appendMediaToMessage(mes, div.closest('.mes'));7223 appendMediaToMessage(mes, div.closest('.mes'));
7154 addCopyToCodeBlocks(div.closest('.mes'));7224 addCopyToCodeBlocks(div.closest('.mes'));
71557225
@@ -7997,11 +8067,25 @@ function updateEditArrowClasses() {
7997 }8067 }
7998}8068}
79998069
8000function closeMessageEditor() {8070/**
8071 * Closes the message editor.
8072 * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'.
8073 */
8074export function closeMessageEditor(what = 'all') {
8075 if (what === 'message' || what === 'all') {
8001 if (this_edit_mes_id) {8076 if (this_edit_mes_id) {
8002 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();8077 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
8003 }8078 }
8004 }8079 }
8080 if (what === 'reasoning' || what === 'all') {
8081 document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => {
8082 const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel');
8083 if (cancelButton instanceof HTMLElement) {
8084 cancelButton.click();
8085 }
8086 });
8087 }
8088}
80058089
8006export function setGenerationProgress(progress) {8090export function setGenerationProgress(progress) {
8007 if (!progress) {8091 if (!progress) {
@@ -8432,6 +8516,9 @@ function swipe_left() { // when we swipe left..but no generation.
8432 streamingProcessor.onStopStreaming();8516 streamingProcessor.onStopStreaming();
8433 }8517 }
84348518
8519 // Make sure ad-hoc changes to extras are saved before swiping away
8520 syncCurrentSwipeInfoExtras();
8521
8435 const swipe_duration = 120;8522 const swipe_duration = 120;
8436 const swipe_range = '700px';8523 const swipe_range = '700px';
8437 chat[chat.length - 1]['swipe_id']--;8524 chat[chat.length - 1]['swipe_id']--;
@@ -8483,7 +8570,8 @@ function swipe_left() { // when we swipe left..but no generation.
8483 }8570 }
84848571
8485 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);8572 const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
8486 const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);8573 const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
8574 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
8487 chat[chat.length - 1]['extra']['token_count'] = tokenCount;8575 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
8488 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);8576 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
8489 }8577 }
@@ -8566,6 +8654,9 @@ const swipe_right = () => {
8566 return unblockGeneration();8654 return unblockGeneration();
8567 }8655 }
85688656
8657 // Make sure ad-hoc changes to extras are saved before swiping away
8658 syncCurrentSwipeInfoExtras();
8659
8569 const swipe_duration = 200;8660 const swipe_duration = 200;
8570 const swipe_range = 700;8661 const swipe_range = 700;
8571 //console.log(swipe_range);8662 //console.log(swipe_range);
@@ -8647,6 +8738,7 @@ const swipe_right = () => {
8647 // resets the timer8738 // resets the timer
8648 swipeMessage.find('.mes_timer').html('');8739 swipeMessage.find('.mes_timer').html('');
8649 swipeMessage.find('.tokenCounterDisplay').text('');8740 swipeMessage.find('.tokenCounterDisplay').text('');
8741 swipeMessage.find('.mes_reasoning').html('');
8650 } else {8742 } else {
8651 //console.log('showing previously generated swipe candidate, or "..."');8743 //console.log('showing previously generated swipe candidate, or "..."');
8652 //console.log('onclick right swipe calling addOneMessage');8744 //console.log('onclick right swipe calling addOneMessage');
@@ -8657,7 +8749,8 @@ const swipe_right = () => {
8657 chat[chat.length - 1].extra = {};8749 chat[chat.length - 1].extra = {};
8658 }8750 }
86598751
8660 const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0);8752 const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes;
8753 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
8661 chat[chat.length - 1]['extra']['token_count'] = tokenCount;8754 chat[chat.length - 1]['extra']['token_count'] = tokenCount;
8662 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);8755 swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
8663 }8756 }
@@ -9459,7 +9552,8 @@ function addDebugFunctions() {
9459 message.extra = {};9552 message.extra = {};
9460 }9553 }
94619554
9462 message.extra.token_count = await getTokenCountAsync(message.mes, 0);9555 const tokenCountText = (message?.extra?.reasoning || '') + message.mes;
9556 message.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
9463 }9557 }
94649558
9465 await saveChatConditional();9559 await saveChatConditional();
@@ -10768,6 +10862,8 @@ jQuery(async function () {
10768 chat[this_edit_mes_id].is_system,10862 chat[this_edit_mes_id].is_system,
10769 chat[this_edit_mes_id].is_user,10863 chat[this_edit_mes_id].is_user,
10770 this_edit_mes_id,10864 this_edit_mes_id,
10865 {},
10866 false,
10771 ));10867 ));
10772 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));10868 appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
10773 addCopyToCodeBlocks($(this).closest('.mes'));10869 addCopyToCodeBlocks($(this).closest('.mes'));
@@ -11228,14 +11324,15 @@ jQuery(async function () {
1122811324
11229 $(document).keyup(function (e) {11325 $(document).keyup(function (e) {
11230 if (e.key === 'Escape') {11326 if (e.key === 'Escape') {
11231 const isEditVisible = $('#curEditTextarea').is(':visible');11327 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
11232 if (isEditVisible && power_user.auto_save_msg_edits === false) {11328 if (isEditVisible && power_user.auto_save_msg_edits === false) {
11233 closeMessageEditor();11329 closeMessageEditor('all');
11234 $('#send_textarea').focus();11330 $('#send_textarea').focus();
11235 return;11331 return;
11236 }11332 }
11237 if (isEditVisible && power_user.auto_save_msg_edits === true) {11333 if (isEditVisible && power_user.auto_save_msg_edits === true) {
11238 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();11334 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
11335 closeMessageEditor('reasoning');
11239 $('#send_textarea').focus();11336 $('#send_textarea').focus();
11240 return;11337 return;
11241 }11338 }
public/scripts/authors-note.js+1 -1
@@ -566,7 +566,7 @@ export function initAuthorsNote() {
566 namedArgumentList: [],566 namedArgumentList: [],
567 unnamedArgumentList: [567 unnamedArgumentList: [
568 new SlashCommandArgument(568 new SlashCommandArgument(
569 'position', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],569 'role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
570 ),570 ),
571 ],571 ],
572 helpString: `572 helpString: `
public/scripts/backgrounds.js+19 -9
@@ -96,8 +96,13 @@ function highlightLockedBackground() {
96 });96 });
97}97}
9898
99/**
100 * Locks the background for the current chat
101 * @param {Event} e Click event
102 * @returns {string} Empty string
103 */
99function onLockBackgroundClick(e) {104function onLockBackgroundClick(e) {
100 e.stopPropagation();105 e?.stopPropagation();
101106
102 const chatName = getCurrentChatId();107 const chatName = getCurrentChatId();
103108
@@ -106,7 +111,7 @@ function onLockBackgroundClick(e) {
106 return '';111 return '';
107 }112 }
108113
109 const relativeBgImage = getUrlParameter(this);114 const relativeBgImage = getUrlParameter(this) ?? background_settings.url;
110115
111 saveBackgroundMetadata(relativeBgImage);116 saveBackgroundMetadata(relativeBgImage);
112 setCustomBackground();117 setCustomBackground();
@@ -114,8 +119,13 @@ function onLockBackgroundClick(e) {
114 return '';119 return '';
115}120}
116121
122/**
123 * Locks the background for the current chat
124 * @param {Event} e Click event
125 * @returns {string} Empty string
126 */
117function onUnlockBackgroundClick(e) {127function onUnlockBackgroundClick(e) {
118 e.stopPropagation();128 e?.stopPropagation();
119 removeBackgroundMetadata();129 removeBackgroundMetadata();
120 unsetCustomBackground();130 unsetCustomBackground();
121 highlightLockedBackground();131 highlightLockedBackground();
@@ -482,10 +492,10 @@ function highlightNewBackground(bg) {
482 */492 */
483function setFittingClass(fitting) {493function setFittingClass(fitting) {
484 const backgrounds = $('#bg1, #bg_custom');494 const backgrounds = $('#bg1, #bg_custom');
485 backgrounds.toggleClass('cover', fitting === 'cover');495 for (const option of ['cover', 'contain', 'stretch', 'center']) {
486 backgrounds.toggleClass('contain', fitting === 'contain');496 backgrounds.toggleClass(option, option === fitting);
487 backgrounds.toggleClass('stretch', fitting === 'stretch');497 }
488 backgrounds.toggleClass('center', fitting === 'center');498 background_settings.fitting = fitting;
489}499}
490500
491function onBackgroundFilterInput() {501function onBackgroundFilterInput() {
@@ -513,12 +523,12 @@ export function initBackgrounds() {
513 $('#add_bg_button').on('change', onBackgroundUploadSelected);523 $('#add_bg_button').on('change', onBackgroundUploadSelected);
514 $('#bg-filter').on('input', onBackgroundFilterInput);524 $('#bg-filter').on('input', onBackgroundFilterInput);
515 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',525 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
516 callback: onLockBackgroundClick,526 callback: () => onLockBackgroundClick(new CustomEvent('click')),
517 aliases: ['bglock'],527 aliases: ['bglock'],
518 helpString: 'Locks a background for the currently selected chat',528 helpString: 'Locks a background for the currently selected chat',
519 }));529 }));
520 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',530 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
521 callback: onUnlockBackgroundClick,531 callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
522 aliases: ['bgunlock'],532 aliases: ['bgunlock'],
523 helpString: 'Unlocks a background for the currently selected chat',533 helpString: 'Unlocks a background for the currently selected chat',
524 }));534 }));
public/scripts/extensions/connection-manager/index.js+4 -0
@@ -30,6 +30,7 @@ const CC_COMMANDS = [
30 'api-url',30 'api-url',
31 'model',31 'model',
32 'proxy',32 'proxy',
33 'stop-strings',
33];34];
3435
35const TC_COMMANDS = [36const TC_COMMANDS = [
@@ -43,6 +44,7 @@ const TC_COMMANDS = [
43 'context',44 'context',
44 'instruct-state',45 'instruct-state',
45 'tokenizer',46 'tokenizer',
47 'stop-strings',
46];48];
4749
48const FANCY_NAMES = {50const FANCY_NAMES = {
@@ -57,6 +59,7 @@ const FANCY_NAMES = {
57 'instruct': 'Instruct Template',59 'instruct': 'Instruct Template',
58 'context': 'Context Template',60 'context': 'Context Template',
59 'tokenizer': 'Tokenizer',61 'tokenizer': 'Tokenizer',
62 'stop-strings': 'Custom Stopping Strings',
60};63};
6164
62/**65/**
@@ -138,6 +141,7 @@ const profilesProvider = () => [
138 * @property {string} [context] Context Template141 * @property {string} [context] Context Template
139 * @property {string} [instruct-state] Instruct Mode142 * @property {string} [instruct-state] Instruct Mode
140 * @property {string} [tokenizer] Tokenizer143 * @property {string} [tokenizer] Tokenizer
144 * @property {string} [stop-strings] Custom Stopping Strings
141 * @property {string[]} [exclude] Commands to exclude145 * @property {string[]} [exclude] Commands to exclude
142 */146 */
143147
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+4 -0
@@ -883,6 +883,10 @@ export class SlashCommandHandler {
883 }883 }
884 }884 }
885 getQuickReply(args) {885 getQuickReply(args) {
886 if (!args.id && !args.label) {
887 toastr.error('Please provide a valid id or label.');
888 return '';
889 }
886 try {890 try {
887 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));891 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
888 } catch (ex) {892 } catch (ex) {
public/scripts/extensions/regex/editor.html+6 -0
@@ -94,6 +94,12 @@
94 <span data-i18n="World Info">World Info</span>94 <span data-i18n="World Info">World Info</span>
95 </label>95 </label>
96 </div>96 </div>
97 <div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
98 <label class="checkbox flex-container">
99 <input type="checkbox" name="replace_position" value="6">
100 <span data-i18n="Reasoning">Reasoning</span>
101 </label>
102 </div>
97 <div class="flex-container wide100p marginTop5">103 <div class="flex-container wide100p marginTop5">
98 <div class="flex1 flex-container flexNoGap">104 <div class="flex1 flex-container flexNoGap">
99 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">105 <small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
public/scripts/extensions/regex/engine.js+2 -1
@@ -20,6 +20,7 @@ const regex_placement = {
20 SLASH_COMMAND: 3,20 SLASH_COMMAND: 3,
21 // 4 - sendAs (legacy)21 // 4 - sendAs (legacy)
22 WORLD_INFO: 5,22 WORLD_INFO: 5,
23 REASONING: 6,
23};24};
2425
25export const substitute_find_regex = {26export const substitute_find_regex = {
@@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
94 // Script applies to Generate and input is Generate95 // Script applies to Generate and input is Generate
95 (script.promptOnly && isPrompt) ||96 (script.promptOnly && isPrompt) ||
96 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand97 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
97 (!script.markdownOnly && !script.promptOnly && !isMarkdown)98 (!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
98 ) {99 ) {
99 if (isEdit && !script.runOnEdit) {100 if (isEdit && !script.runOnEdit) {
100 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);101 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
public/scripts/extensions/regex/index.js+1 -1
@@ -18,7 +18,7 @@ import { t } from '../../i18n.js';
18 * @property {string} replaceString - The replace string18 * @property {string} replaceString - The replace string
19 * @property {string[]} trimStrings - The trim strings19 * @property {string[]} trimStrings - The trim strings
20 * @property {string?} findRegex - The find regex20 * @property {string?} findRegex - The find regex
21 * @property {string?} substituteRegex - The substitute regex21 * @property {number?} substituteRegex - The substitute regex
22 */22 */
2323
24/**24/**
public/scripts/extensions/tts/alltalk.js+10 -6
@@ -388,7 +388,7 @@ class AllTalkTtsProvider {
388 }388 }
389389
390 async fetchRvcVoiceObjects() {390 async fetchRvcVoiceObjects() {
391 if (this.settings.server_version == 'v2') {391 if (this.settings.server_version == 'v1') {
392 console.log('Skipping RVC voices fetch for V1 server');392 console.log('Skipping RVC voices fetch for V1 server');
393 return [];393 return [];
394 }394 }
@@ -1031,14 +1031,18 @@ class AllTalkTtsProvider {
1031 console.error('fetchTtsGeneration Error Response Text:', errorText);1031 console.error('fetchTtsGeneration Error Response Text:', errorText);
1032 throw new Error(`HTTP ${response.status}: ${errorText}`);1032 throw new Error(`HTTP ${response.status}: ${errorText}`);
1033 }1033 }
1034
1034 const data = await response.json();1035 const data = await response.json();
10351036
1036 // Handle V1/V2 URL differences1037 // V1 returns a complete URL, V2 returns a relative path
1037 const outputUrl = this.settings.server_version === 'v1'1038 if (this.settings.server_version === 'v1') {
1038 ? data.output_file_url // V1 returns full URL1039 // V1: Use the complete URL directly from the response
1039 : `${this.settings.provider_endpoint}${data.output_file_url}`; // V2 returns relative path1040 return data.output_file_url;
1041 } else {
1042 // V2: Combine the endpoint with the relative path
1043 return `${this.settings.provider_endpoint}${data.output_file_url}`;
1044 }
10401045
1041 return outputUrl;
1042 } catch (error) {1046 } catch (error) {
1043 console.error('[fetchTtsGeneration] Exception caught:', error);1047 console.error('[fetchTtsGeneration] Exception caught:', error);
1044 throw error;1048 throw error;
public/scripts/extensions/tts/index.js+48 -5
@@ -29,6 +29,7 @@ import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
29import { GoogleTranslateTtsProvider } from './google-translate.js';29import { GoogleTranslateTtsProvider } from './google-translate.js';
3030
31const UPDATE_INTERVAL = 1000;31const UPDATE_INTERVAL = 1000;
32const wrapper = new ModuleWorkerWrapper(moduleWorker);
3233
33let voiceMapEntries = [];34let voiceMapEntries = [];
34let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}35let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
@@ -118,7 +119,7 @@ async function onNarrateOneMessage() {
118 }119 }
119120
120 resetTtsPlayback();121 resetTtsPlayback();
121 ttsJobQueue.push(message);122 processAndQueueTtsMessage(message);
122 moduleWorker();123 moduleWorker();
123}124}
124125
@@ -145,7 +146,7 @@ async function onNarrateText(args, text) {
145 }146 }
146147
147 resetTtsPlayback();148 resetTtsPlayback();
148 ttsJobQueue.push({ mes: text, name: name });149 processAndQueueTtsMessage({ mes: text, name: name });
149 await moduleWorker();150 await moduleWorker();
150151
151 // Return back to the chat voices152 // Return back to the chat voices
@@ -197,6 +198,36 @@ function isTtsProcessing() {
197 return processing;198 return processing;
198}199}
199200
201/**
202 * Splits a message into lines and adds each non-empty line to the TTS job queue.
203 * @param {Object} message - The message object to be processed.
204 * @param {string} message.mes - The text of the message to be split into lines.
205 * @param {string} message.name - The name associated with the message.
206 * @returns {void}
207 */
208function processAndQueueTtsMessage(message) {
209 if (!extension_settings.tts.narrate_by_paragraphs) {
210 ttsJobQueue.push(message);
211 return;
212 }
213
214 const lines = message.mes.split('\n');
215
216 for (let i = 0; i < lines.length; i++) {
217 const line = lines[i];
218
219 if (line.length === 0) {
220 continue;
221 }
222
223 ttsJobQueue.push(
224 Object.assign({}, message, {
225 mes: line,
226 }),
227 );
228 }
229}
230
200function debugTtsPlayback() {231function debugTtsPlayback() {
201 console.log(JSON.stringify(232 console.log(JSON.stringify(
202 {233 {
@@ -326,7 +357,7 @@ function onAudioControlClicked() {
326 resetTtsPlayback();357 resetTtsPlayback();
327 } else {358 } else {
328 // Default play behavior if not processing or playing is to play the last message.359 // Default play behavior if not processing or playing is to play the last message.
329 ttsJobQueue.push(context.chat[context.chat.length - 1]);360 processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
330 }361 }
331 updateUiAudioPlayState();362 updateUiAudioPlayState();
332}363}
@@ -351,6 +382,7 @@ function completeCurrentAudioJob() {
351 audioQueueProcessorReady = true;382 audioQueueProcessorReady = true;
352 currentAudioJob = null;383 currentAudioJob = null;
353 // updateUiPlayState();384 // updateUiPlayState();
385 wrapper.update();
354}386}
355387
356/**388/**
@@ -440,7 +472,7 @@ async function processTtsQueue() {
440 }472 }
441473
442 if (extension_settings.tts.skip_tags) {474 if (extension_settings.tts.skip_tags) {
443 text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim();475 text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim();
444 }476 }
445477
446 if (!extension_settings.tts.pass_asterisks) {478 if (!extension_settings.tts.pass_asterisks) {
@@ -543,6 +575,7 @@ function loadSettings() {
543 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);575 $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
544 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);576 $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
545 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);577 $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
578 $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
546 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);579 $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
547 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);580 $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
548 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);581 $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -612,6 +645,11 @@ function onPeriodicAutoGenerationClick() {
612 saveSettingsDebounced();645 saveSettingsDebounced();
613}646}
614647
648function onNarrateByParagraphsClick() {
649 extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
650 saveSettingsDebounced();
651}
652
615653
616function onNarrateDialoguesClick() {654function onNarrateDialoguesClick() {
617 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');655 extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -790,7 +828,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
790 lastChatId = context.chatId;828 lastChatId = context.chatId;
791829
792 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);830 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
831
832 if (extension_settings.tts.periodic_auto_generation) {
793 ttsJobQueue.push(message);833 ttsJobQueue.push(message);
834 } else {
835 processAndQueueTtsMessage(message);
836 }
794}837}
795838
796async function onMessageDeleted() {839async function onMessageDeleted() {
@@ -1130,6 +1173,7 @@ jQuery(async function () {
1130 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);1173 $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
1131 $('#tts_auto_generation').on('click', onAutoGenerationClick);1174 $('#tts_auto_generation').on('click', onAutoGenerationClick);
1132 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);1175 $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
1176 $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
1133 $('#tts_narrate_user').on('click', onNarrateUserClick);1177 $('#tts_narrate_user').on('click', onNarrateUserClick);
11341178
1135 $('#playback_rate').on('input', function () {1179 $('#playback_rate').on('input', function () {
@@ -1151,7 +1195,6 @@ jQuery(async function () {
1151 loadSettings(); // Depends on Extension Controls and loadTtsProvider1195 loadSettings(); // Depends on Extension Controls and loadTtsProvider
1152 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies1196 loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
1153 addAudioControl(); // Depends on Extension Controls1197 addAudioControl(); // Depends on Extension Controls
1154 const wrapper = new ModuleWorkerWrapper(moduleWorker);
1155 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things1198 setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
1156 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);1199 eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
1157 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);1200 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
public/scripts/extensions/tts/settings.html+4 -0
@@ -30,6 +30,10 @@
30 <input type="checkbox" id="tts_periodic_auto_generation">30 <input type="checkbox" id="tts_periodic_auto_generation">
31 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>31 <small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>
32 </label>32 </label>
33 <label class="checkbox_label" for="tts_narrate_by_paragraphs">
34 <input type="checkbox" id="tts_narrate_by_paragraphs">
35 <small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small>
36 </label>
33 <label class="checkbox_label" for="tts_narrate_quoted">37 <label class="checkbox_label" for="tts_narrate_quoted">
34 <input type="checkbox" id="tts_narrate_quoted">38 <input type="checkbox" id="tts_narrate_quoted">
35 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>39 <small data-i18n="Only narrate quotes">Only narrate "quotes"</small>
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188 if (data?.token) {188 if (data?.token) {
189 text += data.token;189 text += data.token;
190 }190 }
191 yield { text, swipes: [], toolCalls: [] };191 yield { text, swipes: [], toolCalls: [], state: {} };
192 }192 }
193 };193 };
194}194}
public/scripts/loader.js+27 -6
@@ -27,21 +27,42 @@ export async function hideLoader() {
27 }27 }
2828
29 return new Promise((resolve) => {29 return new Promise((resolve) => {
30 // Spinner blurs/fades out30 const spinner = $('#load-spinner');
31 $('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {31 if (!spinner.length) {
32 console.warn('Spinner element not found, skipping animation');
33 cleanup();
34 return;
35 }
36
37 // Check if transitions are enabled
38 const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
39 const hasTransitions = parseFloat(transitionDuration) > 0;
40
41 if (hasTransitions) {
42 Promise.race([
43 new Promise((r) => setTimeout(r, 500)), // Fallback timeout
44 new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
45 ]).finally(cleanup);
46 } else {
47 cleanup();
48 }
49
50 function cleanup() {
32 $('#loader').remove();51 $('#loader').remove();
33 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS52 // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
34 // If it's present, we remove it once and then it's gone.53 // If it's present, we remove it once and then it's gone.
35 yoinkPreloader();54 yoinkPreloader();
3655
37 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {56 loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
57 .catch((err) => console.error('Error completing loaderPopup:', err))
58 .finally(() => {
38 loaderPopup = null;59 loaderPopup = null;
39 resolve();60 resolve();
40 });61 });
41 });62 }
4263
43 $('#load-spinner')64 // Apply the styles
44 .css({65 spinner.css({
45 'filter': 'blur(15px)',66 'filter': 'blur(15px)',
46 'opacity': '0',67 'opacity': '0',
47 });68 });
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746 text += data.token;746 text += data.token;
747 }747 }
748748
749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] };749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} };
750 }750 }
751 };751 };
752}752}
public/scripts/openai.js+31 -34
@@ -258,7 +258,7 @@ const default_settings = {
258 ai21_model: 'jamba-1.5-large',258 ai21_model: 'jamba-1.5-large',
259 mistralai_model: 'mistral-large-latest',259 mistralai_model: 'mistral-large-latest',
260 cohere_model: 'command-r-plus',260 cohere_model: 'command-r-plus',
261 perplexity_model: 'llama-3.1-70b-instruct',261 perplexity_model: 'sonar-pro',
262 groq_model: 'llama-3.1-70b-versatile',262 groq_model: 'llama-3.1-70b-versatile',
263 nanogpt_model: 'gpt-4o-mini',263 nanogpt_model: 'gpt-4o-mini',
264 zerooneai_model: 'yi-large',264 zerooneai_model: 'yi-large',
@@ -298,7 +298,7 @@ const default_settings = {
298 names_behavior: character_names_behavior.DEFAULT,298 names_behavior: character_names_behavior.DEFAULT,
299 continue_postfix: continue_postfix_types.SPACE,299 continue_postfix: continue_postfix_types.SPACE,
300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,300 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
301 show_thoughts: false,301 show_thoughts: true,
302 seed: -1,302 seed: -1,
303 n: 1,303 n: 1,
304};304};
@@ -337,7 +337,7 @@ const oai_settings = {
337 ai21_model: 'jamba-1.5-large',337 ai21_model: 'jamba-1.5-large',
338 mistralai_model: 'mistral-large-latest',338 mistralai_model: 'mistral-large-latest',
339 cohere_model: 'command-r-plus',339 cohere_model: 'command-r-plus',
340 perplexity_model: 'llama-3.1-70b-instruct',340 perplexity_model: 'sonar-pro',
341 groq_model: 'llama-3.1-70b-versatile',341 groq_model: 'llama-3.1-70b-versatile',
342 nanogpt_model: 'gpt-4o-mini',342 nanogpt_model: 'gpt-4o-mini',
343 zerooneai_model: 'yi-large',343 zerooneai_model: 'yi-large',
@@ -377,7 +377,7 @@ const oai_settings = {
377 names_behavior: character_names_behavior.DEFAULT,377 names_behavior: character_names_behavior.DEFAULT,
378 continue_postfix: continue_postfix_types.SPACE,378 continue_postfix: continue_postfix_types.SPACE,
379 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,379 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
380 show_thoughts: false,380 show_thoughts: true,
381 seed: -1,381 seed: -1,
382 n: 1,382 n: 1,
383};383};
@@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
1096 // Unordered prompts without marker1096 // Unordered prompts without marker
1097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },1097 { role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
1098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },1098 { role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
1099 { role: 'system', content: bias, identifier: 'bias' },
1100 { role: 'system', content: groupNudge, identifier: 'groupNudge' },1099 { role: 'system', content: groupNudge, identifier: 'groupNudge' },
1100 { role: 'assistant', content: bias, identifier: 'bias' },
1101 ];1101 ];
11021102
1103 // Tavern Extras - Summary1103 // Tavern Extras - Summary
@@ -1869,7 +1869,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1869 const isQuiet = type === 'quiet';1869 const isQuiet = type === 'quiet';
1870 const isImpersonate = type === 'impersonate';1870 const isImpersonate = type === 'impersonate';
1871 const isContinue = type === 'continue';1871 const isContinue = type === 'continue';
1872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogle && oai_settings.google_model.includes('bison')) && !(isOAI && oai_settings.openai_model.startsWith('o1-'));1872 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isGoogle && oai_settings.google_model.includes('bison')) && !(isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3')));
1873 const useLogprobs = !!power_user.request_token_probabilities;1873 const useLogprobs = !!power_user.request_token_probabilities;
1874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);1874 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
18751875
@@ -1913,7 +1913,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1913 'user_name': name1,1913 'user_name': name1,
1914 'char_name': name2,1914 'char_name': name2,
1915 'group_names': getGroupNames(),1915 'group_names': getGroupNames(),
1916 'show_thoughts': Boolean(oai_settings.show_thoughts),1916 'include_reasoning': Boolean(oai_settings.show_thoughts),
1917 };1917 };
19181918
1919 // Empty array will produce a validation error1919 // Empty array will produce a validation error
@@ -2050,7 +2050,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2050 await ToolManager.registerFunctionToolsOpenAI(generate_data);2050 await ToolManager.registerFunctionToolsOpenAI(generate_data);
2051 }2051 }
20522052
2053 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {2053 if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
2054 generate_data.messages.forEach((msg) => {2054 generate_data.messages.forEach((msg) => {
2055 if (msg.role === 'system') {2055 if (msg.role === 'system') {
2056 msg.role = 'user';2056 msg.role = 'user';
@@ -2095,7 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2095 let text = '';2095 let text = '';
2096 const swipes = [];2096 const swipes = [];
2097 const toolCalls = [];2097 const toolCalls = [];
2098 const state = {};2098 const state = { reasoning: '' };
2099 while (true) {2099 while (true) {
2100 const { done, value } = await reader.read();2100 const { done, value } = await reader.read();
2101 if (done) return;2101 if (done) return;
@@ -2113,7 +2113,7 @@ async function sendOpenAIRequest(type, messages, signal) {
21132113
2114 ToolManager.parseToolCalls(toolCalls, parsed);2114 ToolManager.parseToolCalls(toolCalls, parsed);
21152115
2116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };2116 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
2117 }2117 }
2118 };2118 };
2119 }2119 }
@@ -2150,16 +2150,22 @@ function getStreamingReply(data, state) {
2150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {2150 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
2151 return data?.delta?.text || '';2151 return data?.delta?.text || '';
2152 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {2152 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
2153 return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || '';2153 if (oai_settings.show_thoughts) {
2154 state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
2155 }
2156 return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
2154 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {2157 } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
2155 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';2158 return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
2156 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {2159 } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
2157 const hadThoughts = state.hadThoughts;2160 if (oai_settings.show_thoughts) {
2158 const thoughts = data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '';2161 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2159 const content = data.choices?.[0]?.delta?.content || '';2162 }
2160 state.hadThoughts = !!thoughts;2163 return data.choices?.[0]?.delta?.content || '';
2161 const separator = hadThoughts && !thoughts ? '\n\n' : '';2164 } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2162 return [thoughts, separator, content].filter(x => x).join('\n\n');2165 if (oai_settings.show_thoughts) {
2166 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2167 }
2168 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2163 } else {2169 } else {
2164 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2165 }2171 }
@@ -4021,7 +4027,7 @@ function getMaxContextOpenAI(value) {
4021 if (oai_settings.max_context_unlocked) {4027 if (oai_settings.max_context_unlocked) {
4022 return unlocked_max;4028 return unlocked_max;
4023 }4029 }
4024 else if (value.startsWith('o1-')) {4030 else if (value.startsWith('o1') || value.startsWith('o3')) {
4025 return max_128k;4031 return max_128k;
4026 }4032 }
4027 else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {4033 else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
@@ -4374,28 +4380,19 @@ async function onModelChange() {
4374 if (oai_settings.max_context_unlocked) {4380 if (oai_settings.max_context_unlocked) {
4375 $('#openai_max_context').attr('max', unlocked_max);4381 $('#openai_max_context').attr('max', unlocked_max);
4376 }4382 }
4383 else if (['sonar', 'sonar-reasoning'].includes(oai_settings.perplexity_model)) {
4384 $('#openai_max_context').attr('max', 127000);
4385 }
4386 else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
4387 $('#openai_max_context').attr('max', 200000);
4388 }
4377 else if (oai_settings.perplexity_model.includes('llama-3.1')) {4389 else if (oai_settings.perplexity_model.includes('llama-3.1')) {
4378 const isOnline = oai_settings.perplexity_model.includes('online');4390 const isOnline = oai_settings.perplexity_model.includes('online');
4379 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;4391 const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
4380 $('#openai_max_context').attr('max', contextSize);4392 $('#openai_max_context').attr('max', contextSize);
4381 }4393 }
4382 else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
4383 $('#openai_max_context').attr('max', max_32k);
4384 }
4385 else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
4386 $('#openai_max_context').attr('max', 28000);
4387 }
4388 else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) {
4389 $('#openai_max_context').attr('max', max_16k);
4390 }
4391 else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
4392 $('#openai_max_context').attr('max', max_8k);
4393 }
4394 else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
4395 $('#openai_max_context').attr('max', 12000);
4396 }
4397 else {4394 else {
4398 $('#openai_max_context').attr('max', max_4k);4395 $('#openai_max_context').attr('max', max_128k);
4399 }4396 }
4400 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);4397 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4401 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');4398 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
public/scripts/power-user.js+92 -2
@@ -253,6 +253,15 @@ let power_user = {
253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',253 content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
254 },254 },
255255
256 reasoning: {
257 auto_parse: false,
258 add_to_prompts: false,
259 prefix: '<think>\n',
260 suffix: '\n</think>',
261 separator: '\n\n',
262 max_additions: 1,
263 },
264
256 personas: {},265 personas: {},
257 default_persona: null,266 default_persona: null,
258 persona_descriptions: {},267 persona_descriptions: {},
@@ -2910,6 +2919,46 @@ export function flushEphemeralStoppingStrings() {
2910}2919}
29112920
2912/**2921/**
2922 * Checks if the generated text should be filtered based on the auto-swipe settings.
2923 * @param {string} text The text to check
2924 * @returns {boolean} If the generated text should be filtered
2925 */
2926export function generatedTextFiltered(text) {
2927 /**
2928 * Checks if the given text contains any of the blacklisted words.
2929 * @param {string} text The text to check
2930 * @param {string[]} blacklist The list of blacklisted words
2931 * @param {number} threshold The number of blacklisted words that need to be present to trigger the check
2932 * @returns {boolean} Whether the text contains blacklisted words
2933 */
2934 function containsBlacklistedWords(text, blacklist, threshold) {
2935 const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
2936 const matches = text.match(regex) || [];
2937 return matches.length >= threshold;
2938 }
2939
2940 // Make sure a generated text is non-empty
2941 // Otherwise we might get in a loop with a broken API
2942 text = text.trim();
2943 if (text.length > 0) {
2944 if (power_user.auto_swipe_minimum_length) {
2945 if (text.length < power_user.auto_swipe_minimum_length) {
2946 console.log('Generated text size too small');
2947 return true;
2948 }
2949 }
2950 if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
2951 if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
2952 console.log('Generated text has blacklisted words');
2953 return true;
2954 }
2955 }
2956 }
2957
2958 return false;
2959}
2960
2961/**
2913 * Gets the custom stopping strings from the power user settings.2962 * Gets the custom stopping strings from the power user settings.
2914 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.2963 * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
2915 * @returns {string[]} An array of custom stopping strings2964 * @returns {string[]} An array of custom stopping strings
@@ -3880,9 +3929,9 @@ $(document).ready(() => {
3880 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',3929 helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
3881 }));3930 }));
3882 SlashCommandParser.addCommandObject(SlashCommand.fromProps({3931 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3883 name: 'delmode',3932 name: 'del',
3884 callback: doDelMode,3933 callback: doDelMode,
3885 aliases: ['del'],3934 aliases: ['delete', 'delmode'],
3886 unnamedArgumentList: [3935 unnamedArgumentList: [
3887 new SlashCommandArgument(3936 new SlashCommandArgument(
3888 'optional number', [ARGUMENT_TYPE.NUMBER], false,3937 'optional number', [ARGUMENT_TYPE.NUMBER], false,
@@ -4065,4 +4114,45 @@ $(document).ready(() => {
4065 ],4114 ],
4066 helpString: 'activates a movingUI preset by name',4115 helpString: 'activates a movingUI preset by name',
4067 }));4116 }));
4117 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4118 name: 'stop-strings',
4119 aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
4120 helpString: `
4121 <div>
4122 Sets a list of custom stopping strings. Gets the list if no value is provided.
4123 </div>
4124 <div>
4125 <strong>Examples:</strong>
4126 </div>
4127 <ul>
4128 <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4129 <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4130 </ul>
4131 `,
4132 returns: ARGUMENT_TYPE.LIST,
4133 unnamedArgumentList: [
4134 SlashCommandArgument.fromProps({
4135 description: 'list of strings',
4136 typeList: [ARGUMENT_TYPE.LIST],
4137 acceptsMultiple: false,
4138 isRequired: false,
4139 }),
4140 ],
4141 callback: (_, value) => {
4142 if (String(value ?? '').trim()) {
4143 const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4144 if (!parsedValue || !Array.isArray(parsedValue)) {
4145 throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
4146 }
4147 parsedValue.forEach((item, index) => {
4148 parsedValue[index] = String(item);
4149 });
4150 power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4151 $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4152 saveSettingsDebounced();
4153 }
4154
4155 return power_user.custom_stopping_strings;
4156 },
4157 }));
4068});4158});
public/scripts/preset-manager.js+1 -0
@@ -586,6 +586,7 @@ class PresetManager {
586 'tabby_model',586 'tabby_model',
587 'derived',587 'derived',
588 'generic_model',588 'generic_model',
589 'include_reasoning',
589 ];590 ];
590 const settings = Object.assign({}, getSettingsByApiId(this.apiId));591 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
591592
public/scripts/reasoning.js+448 -0
@@ -0,0 +1,448 @@
1import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
3import { t } from './i18n.js';
4import { MacrosParser } from './macros.js';
5import { Popup } from './popup.js';
6import { power_user } from './power-user.js';
7import { SlashCommand } from './slash-commands/SlashCommand.js';
8import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
9import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
10import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
11import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
12
13/**
14 * Gets a message from a jQuery element.
15 * @param {Element} element
16 * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
17 */
18function getMessageFromJquery(element) {
19 const messageBlock = $(element).closest('.mes');
20 const messageId = Number(messageBlock.attr('mesid'));
21 const message = chat[messageId];
22 return { messageId: messageId, message, messageBlock };
23}
24
25/**
26 * Helper class for adding reasoning to messages.
27 * Keeps track of the number of reasoning additions.
28 */
29export class PromptReasoning {
30 static REASONING_PLACEHOLDER = '\u200B';
31 static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
32
33 constructor() {
34 this.counter = 0;
35 }
36
37 /**
38 * Checks if the limit of reasoning additions has been reached.
39 * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
40 */
41 isLimitReached() {
42 if (!power_user.reasoning.add_to_prompts) {
43 return true;
44 }
45
46 return this.counter >= power_user.reasoning.max_additions;
47 }
48
49 /**
50 * Add reasoning to a message according to the power user settings.
51 * @param {string} content Message content
52 * @param {string} reasoning Message reasoning
53 * @param {boolean} isPrefix Whether this is the last message prefix
54 * @returns {string} Message content with reasoning
55 */
56 addToMessage(content, reasoning, isPrefix) {
57 // Disabled or reached limit of additions
58 if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
59 return content;
60 }
61
62 // No reasoning provided or a placeholder
63 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
64 return content;
65 }
66
67 // Increment the counter
68 this.counter++;
69
70 // Substitute macros in variable parts
71 const prefix = substituteParams(power_user.reasoning.prefix || '');
72 const separator = substituteParams(power_user.reasoning.separator || '');
73 const suffix = substituteParams(power_user.reasoning.suffix || '');
74
75 // Combine parts with reasoning only
76 if (isPrefix && !content) {
77 return `${prefix}${reasoning}`;
78 }
79
80 // Combine parts with reasoning and content
81 return `${prefix}${reasoning}${suffix}${separator}${content}`;
82 }
83}
84
85function loadReasoningSettings() {
86 $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
87 $('#reasoning_add_to_prompts').on('change', function () {
88 power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
89 saveSettingsDebounced();
90 });
91
92 $('#reasoning_prefix').val(power_user.reasoning.prefix);
93 $('#reasoning_prefix').on('input', function () {
94 power_user.reasoning.prefix = String($(this).val());
95 saveSettingsDebounced();
96 });
97
98 $('#reasoning_suffix').val(power_user.reasoning.suffix);
99 $('#reasoning_suffix').on('input', function () {
100 power_user.reasoning.suffix = String($(this).val());
101 saveSettingsDebounced();
102 });
103
104 $('#reasoning_separator').val(power_user.reasoning.separator);
105 $('#reasoning_separator').on('input', function () {
106 power_user.reasoning.separator = String($(this).val());
107 saveSettingsDebounced();
108 });
109
110 $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
111 $('#reasoning_max_additions').on('input', function () {
112 power_user.reasoning.max_additions = Number($(this).val());
113 saveSettingsDebounced();
114 });
115
116 $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
117 $('#reasoning_auto_parse').on('change', function () {
118 power_user.reasoning.auto_parse = !!$(this).prop('checked');
119 saveSettingsDebounced();
120 });
121}
122
123function registerReasoningSlashCommands() {
124 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
125 name: 'reasoning-get',
126 aliases: ['get-reasoning'],
127 returns: ARGUMENT_TYPE.STRING,
128 helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,
129 unnamedArgumentList: [
130 SlashCommandArgument.fromProps({
131 description: 'Message ID. If not provided, the message ID of the last message is used.',
132 typeList: ARGUMENT_TYPE.NUMBER,
133 enumProvider: commonEnumProviders.messages(),
134 }),
135 ],
136 callback: (_args, value) => {
137 const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1;
138 const message = chat[messageId];
139 const reasoning = String(message?.extra?.reasoning ?? '');
140 return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
141 },
142 }));
143
144 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
145 name: 'reasoning-set',
146 aliases: ['set-reasoning'],
147 returns: ARGUMENT_TYPE.STRING,
148 helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
149 namedArgumentList: [
150 SlashCommandNamedArgument.fromProps({
151 name: 'at',
152 description: 'Message ID. If not provided, the message ID of the last message is used.',
153 typeList: ARGUMENT_TYPE.NUMBER,
154 enumProvider: commonEnumProviders.messages(),
155 }),
156 ],
157 unnamedArgumentList: [
158 SlashCommandArgument.fromProps({
159 description: 'Reasoning block content.',
160 typeList: ARGUMENT_TYPE.STRING,
161 }),
162 ],
163 callback: async (args, value) => {
164 const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
165 const message = chat[messageId];
166 if (!message?.extra) {
167 return '';
168 }
169
170 message.extra.reasoning = String(value ?? '');
171 await saveChatConditional();
172
173 closeMessageEditor('reasoning');
174 updateMessageBlock(messageId, message);
175 return message.extra.reasoning;
176 },
177 }));
178
179 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
180 name: 'reasoning-parse',
181 aliases: ['parse-reasoning'],
182 returns: 'reasoning string',
183 helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
184 namedArgumentList: [
185 SlashCommandNamedArgument.fromProps({
186 name: 'regex',
187 description: 'Whether to apply regex scripts to the reasoning content.',
188 typeList: [ARGUMENT_TYPE.BOOLEAN],
189 defaultValue: 'true',
190 isRequired: false,
191 enumProvider: commonEnumProviders.boolean('trueFalse'),
192 }),
193 ],
194 unnamedArgumentList: [
195 SlashCommandArgument.fromProps({
196 description: 'input string',
197 typeList: [ARGUMENT_TYPE.STRING],
198 }),
199 ],
200 callback: (args, value) => {
201 if (!value) {
202 return '';
203 }
204
205 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
206 toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
207 return String(value);
208 }
209
210 const parsedReasoning = parseReasoningFromString(String(value));
211
212 if (!parsedReasoning) {
213 return '';
214 }
215
216 const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
217 return applyRegex
218 ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
219 : parsedReasoning.reasoning;
220 },
221 }));
222}
223
224function registerReasoningMacros() {
225 MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`);
226 MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`);
227 MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
228}
229
230function setReasoningEventHandlers() {
231 $(document).on('click', '.mes_reasoning_copy', (e) => {
232 e.stopPropagation();
233 e.preventDefault();
234 });
235
236 $(document).on('click', '.mes_reasoning_edit', function (e) {
237 e.stopPropagation();
238 e.preventDefault();
239 const { message, messageBlock } = getMessageFromJquery(this);
240 if (!message?.extra) {
241 return;
242 }
243
244 const reasoning = String(message?.extra?.reasoning ?? '');
245 const chatElement = document.getElementById('chat');
246 const textarea = document.createElement('textarea');
247 const reasoningBlock = messageBlock.find('.mes_reasoning');
248 textarea.classList.add('reasoning_edit_textarea');
249 textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
250 $(textarea).insertBefore(reasoningBlock);
251
252 if (!CSS.supports('field-sizing', 'content')) {
253 const resetHeight = function () {
254 const scrollTop = chatElement.scrollTop;
255 textarea.style.height = '0px';
256 textarea.style.height = `${textarea.scrollHeight}px`;
257 chatElement.scrollTop = scrollTop;
258 };
259
260 textarea.addEventListener('input', resetHeight);
261 resetHeight();
262 }
263
264 textarea.focus();
265 textarea.setSelectionRange(textarea.value.length, textarea.value.length);
266
267 const textareaRect = textarea.getBoundingClientRect();
268 const chatRect = chatElement.getBoundingClientRect();
269
270 // Scroll if textarea bottom is below visible area
271 if (textareaRect.bottom > chatRect.bottom) {
272 const scrollOffset = textareaRect.bottom - chatRect.bottom;
273 chatElement.scrollTop += scrollOffset;
274 }
275 });
276
277 $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
278 e.stopPropagation();
279 e.preventDefault();
280 const { message, messageId, messageBlock } = getMessageFromJquery(this);
281 if (!message?.extra) {
282 return;
283 }
284
285 const textarea = messageBlock.find('.reasoning_edit_textarea');
286 const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
287 message.extra.reasoning = reasoning;
288 await saveChatConditional();
289 updateMessageBlock(messageId, message);
290 textarea.remove();
291 });
292
293 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
294 e.stopPropagation();
295 e.preventDefault();
296
297 const { messageBlock } = getMessageFromJquery(this);
298 const textarea = messageBlock.find('.reasoning_edit_textarea');
299 textarea.remove();
300 });
301
302 $(document).on('click', '.mes_edit_add_reasoning', async function () {
303 const { message, messageId } = getMessageFromJquery(this);
304 if (!message?.extra) {
305 return;
306 }
307
308 if (message.extra.reasoning) {
309 toastr.info(t`Reasoning already exists.`, t`Edit Message`);
310 return;
311 }
312
313 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
314 await saveChatConditional();
315 closeMessageEditor();
316 updateMessageBlock(messageId, message);
317 });
318
319 $(document).on('click', '.mes_reasoning_delete', async function (e) {
320 e.stopPropagation();
321 e.preventDefault();
322
323 const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
324
325 if (!confirm) {
326 return;
327 }
328
329 const { message, messageId } = getMessageFromJquery(this);
330 if (!message?.extra) {
331 return;
332 }
333 message.extra.reasoning = '';
334 await saveChatConditional();
335 updateMessageBlock(messageId, message);
336 });
337
338 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
339 const { message } = getMessageFromJquery(this);
340 const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
341
342 if (!reasoning) {
343 return;
344 }
345
346 await copyText(reasoning);
347 toastr.info(t`Copied!`, '', { timeOut: 2000 });
348 });
349}
350
351/**
352 * Parses reasoning from a string using the power user reasoning settings.
353 * @typedef {Object} ParsedReasoning
354 * @property {string} reasoning Reasoning block
355 * @property {string} content Message content
356 * @param {string} str Content of the message
357 * @returns {ParsedReasoning|null} Parsed reasoning block and message content
358 */
359function parseReasoningFromString(str) {
360 // Both prefix and suffix must be defined
361 if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
362 return null;
363 }
364
365 try {
366 const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
367
368 let didReplace = false;
369 let reasoning = '';
370 let content = String(str).replace(regex, (_match, captureGroup) => {
371 didReplace = true;
372 reasoning = captureGroup;
373 return '';
374 });
375
376 if (didReplace && power_user.trim_spaces) {
377 reasoning = reasoning.trim();
378 content = content.trim();
379 }
380
381 return { reasoning, content };
382 } catch (error) {
383 console.error('[Reasoning] Error parsing reasoning block', error);
384 return null;
385 }
386}
387
388function registerReasoningAppEvents() {
389 eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
390 if (!power_user.reasoning.auto_parse) {
391 return;
392 }
393
394 console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
395 const message = chat[idx];
396
397 if (!message) {
398 console.warn('[Reasoning] Message not found', idx);
399 return null;
400 }
401
402 if (!message.mes || message.mes === '...') {
403 console.debug('[Reasoning] Message content is empty or a placeholder', idx);
404 return null;
405 }
406
407 const parsedReasoning = parseReasoningFromString(message.mes);
408
409 // No reasoning block found
410 if (!parsedReasoning) {
411 return;
412 }
413
414 // Make sure the message has an extra object
415 if (!message.extra || typeof message.extra !== 'object') {
416 message.extra = {};
417 }
418
419 const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
420
421 // If reasoning was found, add it to the message
422 if (parsedReasoning.reasoning) {
423 message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
424 }
425
426 // Update the message text if it was changed
427 if (parsedReasoning.content !== message.mes) {
428 message.mes = parsedReasoning.content;
429 }
430
431 // Find if a message already exists in DOM and must be updated
432 if (contentUpdated) {
433 const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
434 if (messageRendered) {
435 console.debug('[Reasoning] Updating message block', idx);
436 updateMessageBlock(idx, message);
437 }
438 }
439 });
440}
441
442export function initReasoning() {
443 loadReasoningSettings();
444 setReasoningEventHandlers();
445 registerReasoningSlashCommands();
446 registerReasoningMacros();
447 registerReasoningAppEvents();
448}
public/scripts/slash-commands.js+68 -33
@@ -42,6 +42,7 @@ import {
42 showMoreMessages,42 showMoreMessages,
43 stopGeneration,43 stopGeneration,
44 substituteParams,44 substituteParams,
45 syncCurrentSwipeInfoExtras,
45 system_avatar,46 system_avatar,
46 system_message_types,47 system_message_types,
47 this_chid,48 this_chid,
@@ -58,7 +59,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
58import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';59import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
59import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';60import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
60import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';61import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
61import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';62import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
62import { registerVariableCommands, resolveVariable } from './variables.js';63import { registerVariableCommands, resolveVariable } from './variables.js';
63import { background_settings } from './backgrounds.js';64import { background_settings } from './backgrounds.js';
64import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';65import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -1902,7 +1903,7 @@ export function initDefaultSlashCommands() {
1902 returns: 'uppercase string',1903 returns: 'uppercase string',
1903 unnamedArgumentList: [1904 unnamedArgumentList: [
1904 new SlashCommandArgument(1905 new SlashCommandArgument(
1905 'string', [ARGUMENT_TYPE.STRING], true, false,1906 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1906 ),1907 ),
1907 ],1908 ],
1908 helpString: 'Converts the provided string to uppercase.',1909 helpString: 'Converts the provided string to uppercase.',
@@ -1914,7 +1915,7 @@ export function initDefaultSlashCommands() {
1914 returns: 'lowercase string',1915 returns: 'lowercase string',
1915 unnamedArgumentList: [1916 unnamedArgumentList: [
1916 new SlashCommandArgument(1917 new SlashCommandArgument(
1917 'string', [ARGUMENT_TYPE.STRING], true, false,1918 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1918 ),1919 ),
1919 ],1920 ],
1920 helpString: 'Converts the provided string to lowercase.',1921 helpString: 'Converts the provided string to lowercase.',
@@ -1934,7 +1935,7 @@ export function initDefaultSlashCommands() {
1934 ],1935 ],
1935 unnamedArgumentList: [1936 unnamedArgumentList: [
1936 new SlashCommandArgument(1937 new SlashCommandArgument(
1937 'string', [ARGUMENT_TYPE.STRING], true, false,1938 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
1938 ),1939 ),
1939 ],1940 ],
1940 helpString: `1941 helpString: `
@@ -1998,6 +1999,62 @@ export function initDefaultSlashCommands() {
1998 return '';1999 return '';
1999 },2000 },
2000 }));2001 }));
2002 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2003 name: 'replace',
2004 aliases: ['re'],
2005 callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
2006 if (pattern === '')
2007 throw new Error('Argument of \'pattern=\' cannot be empty');
2008 switch (mode) {
2009 case 'literal':
2010 return text.replaceAll(pattern, replacer);
2011 case 'regex':
2012 return text.replace(regexFromString(pattern), replacer);
2013 default:
2014 throw new Error('Invalid \'/replace mode=\' argument specified!');
2015 }
2016 }),
2017 returns: 'replaced text',
2018 namedArgumentList: [
2019 SlashCommandNamedArgument.fromProps({
2020 name: 'mode',
2021 description: 'Replaces occurrence(s) of a pattern',
2022 typeList: [ARGUMENT_TYPE.STRING],
2023 defaultValue: 'literal',
2024 enumList: ['literal', 'regex'],
2025 }),
2026 new SlashCommandNamedArgument(
2027 'pattern', 'pattern to search with', [ARGUMENT_TYPE.STRING], true, false,
2028 ),
2029 new SlashCommandNamedArgument(
2030 'replacer', 'replacement text for matches', [ARGUMENT_TYPE.STRING], false, false, '',
2031 ),
2032 ],
2033 unnamedArgumentList: [
2034 new SlashCommandArgument(
2035 'text to affect', [ARGUMENT_TYPE.STRING], true, false,
2036 ),
2037 ],
2038 helpString: `
2039 <div>
2040 Replaces text within the provided string based on the pattern.
2041 </div>
2042 <div>
2043 If <code>mode</code> is <code>literal</code> (or omitted), <code>pattern</code> is a literal search string (case-sensitive).<br />
2044 If <code>mode</code> is <code>regex</code>, <code>pattern</code> is parsed as an ECMAScript Regular Expression.<br />
2045 The <code>replacer</code> replaces based on the <code>pattern</code> in the input text.<br />
2046 If <code>replacer</code> is omitted, the replacement(s) will be an empty string.<br />
2047 </div>
2048 <div>
2049 <strong>Example:</strong>
2050 <pre>/let x Blue house and blue car || </pre>
2051 <pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre>
2052 <pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre>
2053 <pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre>
2054 <pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre>
2055 </div>
2056 `,
2057 }));
20012058
2002 registerVariableCommands();2059 registerVariableCommands();
2003}2060}
@@ -2814,8 +2871,11 @@ async function addSwipeCallback(args, value) {
2814 const newSwipeId = lastMessage.swipes.length - 1;2871 const newSwipeId = lastMessage.swipes.length - 1;
28152872
2816 if (isTrueBoolean(args.switch)) {2873 if (isTrueBoolean(args.switch)) {
2874 // Make sure ad-hoc changes to extras are saved before swiping away
2875 syncCurrentSwipeInfoExtras();
2817 lastMessage.swipe_id = newSwipeId;2876 lastMessage.swipe_id = newSwipeId;
2818 lastMessage.mes = lastMessage.swipes[newSwipeId];2877 lastMessage.mes = lastMessage.swipes[newSwipeId];
2878 lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
2819 }2879 }
28202880
2821 await saveChatConditional();2881 await saveChatConditional();
@@ -3081,11 +3141,6 @@ async function removeGroupMemberCallback(_, arg) {
3081 return '';3141 return '';
3082 }3142 }
30833143
3084 if (is_group_generating) {
3085 toastr.warning('Cannot run /memberremove command while the group reply is generating.');
3086 return '';
3087 }
3088
3089 const chid = findGroupMemberId(arg);3144 const chid = findGroupMemberId(arg);
30903145
3091 if (chid === undefined) {3146 if (chid === undefined) {
@@ -3190,12 +3245,7 @@ function findPersonaByName(name) {
3190}3245}
31913246
3192async function sendUserMessageCallback(args, text) {3247async function sendUserMessageCallback(args, text) {
3193 if (!text) {3248 text = String(text ?? '').trim();
3194 toastr.warning('You must specify text to send');
3195 return;
3196 }
3197
3198 text = text.trim();
3199 const compact = isTrueBoolean(args?.compact);3249 const compact = isTrueBoolean(args?.compact);
3200 const bias = extractMessageBias(text);3250 const bias = extractMessageBias(text);
32013251
@@ -3504,13 +3554,7 @@ export function getNameAndAvatarForMessage(character, name = null) {
3504}3554}
35053555
3506export async function sendMessageAs(args, text) {3556export async function sendMessageAs(args, text) {
3507 if (!text) {
3508 toastr.warning('You must specify text to send as');
3509 return '';
3510 }
3511
3512 let name = args.name?.trim();3557 let name = args.name?.trim();
3513 let mesText;
35143558
3515 if (!name) {3559 if (!name) {
3516 const namelessWarningKey = 'sendAsNamelessWarningShown';3560 const namelessWarningKey = 'sendAsNamelessWarningShown';
@@ -3521,7 +3565,7 @@ export async function sendMessageAs(args, text) {
3521 name = name2;3565 name = name2;
3522 }3566 }
35233567
3524 mesText = text.trim();3568 let mesText = String(text ?? '').trim();
35253569
3526 // Requires a regex check after the slash command is pushed to output3570 // Requires a regex check after the slash command is pushed to output
3527 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });3571 mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name });
@@ -3599,11 +3643,7 @@ export async function sendMessageAs(args, text) {
3599}3643}
36003644
3601export async function sendNarratorMessage(args, text) {3645export async function sendNarratorMessage(args, text) {
3602 if (!text) {3646 text = String(text ?? '');
3603 toastr.warning('You must specify text to send');
3604 return '';
3605 }
3606
3607 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;3647 const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
3608 // Messages that do nothing but set bias will be hidden from the context3648 // Messages that do nothing but set bias will be hidden from the context
3609 const bias = extractMessageBias(text);3649 const bias = extractMessageBias(text);
@@ -3694,18 +3734,13 @@ export async function promptQuietForLoudResponse(who, text) {
3694}3734}
36953735
3696async function sendCommentMessage(args, text) {3736async function sendCommentMessage(args, text) {
3697 if (!text) {
3698 toastr.warning('You must specify text to send');
3699 return '';
3700 }
3701
3702 const compact = isTrueBoolean(args?.compact);3737 const compact = isTrueBoolean(args?.compact);
3703 const message = {3738 const message = {
3704 name: COMMENT_NAME_DEFAULT,3739 name: COMMENT_NAME_DEFAULT,
3705 is_user: false,3740 is_user: false,
3706 is_system: true,3741 is_system: true,
3707 send_date: getMessageTimeStamp(),3742 send_date: getMessageTimeStamp(),
3708 mes: substituteParams(text.trim()),3743 mes: substituteParams(String(text ?? '').trim()),
3709 force_avatar: comment_avatar,3744 force_avatar: comment_avatar,
3710 extra: {3745 extra: {
3711 type: system_message_types.COMMENT,3746 type: system_message_types.COMMENT,
public/scripts/sse-stream.js+15 -0
@@ -235,6 +235,21 @@ async function* parseStreamData(json) {
235 }235 }
236 return;236 return;
237 }237 }
238 else if (typeof json.choices[0].delta.reasoning === 'string' && json.choices[0].delta.reasoning.length > 0) {
239 for (let j = 0; j < json.choices[0].delta.reasoning.length; j++) {
240 const str = json.choices[0].delta.reasoning[j];
241 const isLastSymbol = j === json.choices[0].delta.reasoning.length - 1;
242 const choiceClone = structuredClone(json.choices[0]);
243 choiceClone.delta.reasoning = str;
244 choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
245 const choices = [choiceClone];
246 yield {
247 data: { ...json, choices },
248 chunk: str,
249 };
250 }
251 return;
252 }
238 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
239 for (let j = 0; j < json.choices[0].delta.content.length; j++) {254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
240 const str = json.choices[0].delta.content[j];255 const str = json.choices[0].delta.content[j];
public/scripts/st-context.js+15 -0
@@ -1,6 +1,7 @@
1import {1import {
2 activateSendButtons,2 activateSendButtons,
3 addOneMessage,3 addOneMessage,
4 appendMediaToMessage,
4 callPopup,5 callPopup,
5 characters,6 characters,
6 chat,7 chat,
@@ -41,6 +42,7 @@ import {
41 substituteParamsExtended,42 substituteParamsExtended,
42 this_chid,43 this_chid,
43 updateChatMetadata,44 updateChatMetadata,
45 updateMessageBlock,
44} from '../script.js';46} from '../script.js';
45import {47import {
46 extension_settings,48 extension_settings,
@@ -67,6 +69,7 @@ import { textgenerationwebui_settings } from './textgen-settings.js';
67import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';69import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
68import { ToolManager } from './tool-calling.js';70import { ToolManager } from './tool-calling.js';
69import { timestampToMoment, uuidv4 } from './utils.js';71import { timestampToMoment, uuidv4 } from './utils.js';
72import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
7073
71export function getContext() {74export function getContext() {
72 return {75 return {
@@ -171,6 +174,18 @@ export function getContext() {
171 getCharacters,174 getCharacters,
172 uuidv4,175 uuidv4,
173 humanizedDateTime,176 humanizedDateTime,
177 updateMessageBlock,
178 appendMediaToMessage,
179 variables: {
180 local: {
181 get: getLocalVariable,
182 set: setLocalVariable,
183 },
184 global: {
185 get: getGlobalVariable,
186 set: setGlobalVariable,
187 },
188 },
174 };189 };
175}190}
176191
public/scripts/textgen-models.js+6 -0
@@ -54,6 +54,12 @@ const OPENROUTER_PROVIDERS = [
54 'xAI',54 'xAI',
55 'Cloudflare',55 'Cloudflare',
56 'SF Compute',56 'SF Compute',
57 'Minimax',
58 'Nineteen',
59 'Liquid',
60 'Nebius',
61 'Chutes',
62 'Kluster',
57 '01.AI',63 '01.AI',
58 'HuggingFace',64 'HuggingFace',
59 'Mancer',65 'Mancer',
public/scripts/textgen-settings.js+8 -2
@@ -172,6 +172,7 @@ const settings = {
172 //truncation_length: 2048,172 //truncation_length: 2048,
173 ban_eos_token: false,173 ban_eos_token: false,
174 skip_special_tokens: true,174 skip_special_tokens: true,
175 include_reasoning: true,
175 streaming: false,176 streaming: false,
176 mirostat_mode: 0,177 mirostat_mode: 0,
177 mirostat_tau: 5,178 mirostat_tau: 5,
@@ -263,6 +264,7 @@ export const setting_names = [
263 'add_bos_token',264 'add_bos_token',
264 'ban_eos_token',265 'ban_eos_token',
265 'skip_special_tokens',266 'skip_special_tokens',
267 'include_reasoning',
266 'streaming',268 'streaming',
267 'mirostat_mode',269 'mirostat_mode',
268 'mirostat_tau',270 'mirostat_tau',
@@ -501,7 +503,7 @@ export function loadTextGenSettings(data, loadedSettings) {
501 for (const [type, selector] of Object.entries(SERVER_INPUTS)) {503 for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
502 const control = $(selector);504 const control = $(selector);
503 control.val(settings.server_urls[type] ?? '').on('input', function () {505 control.val(settings.server_urls[type] ?? '').on('input', function () {
504 settings.server_urls[type] = String($(this).val());506 settings.server_urls[type] = String($(this).val()).trim();
505 saveSettingsDebounced();507 saveSettingsDebounced();
506 });508 });
507 }509 }
@@ -740,6 +742,7 @@ jQuery(function () {
740 'add_bos_token_textgenerationwebui': true,742 'add_bos_token_textgenerationwebui': true,
741 'temperature_last_textgenerationwebui': true,743 'temperature_last_textgenerationwebui': true,
742 'skip_special_tokens_textgenerationwebui': true,744 'skip_special_tokens_textgenerationwebui': true,
745 'include_reasoning_textgenerationwebui': true,
743 'top_a_textgenerationwebui': 0,746 'top_a_textgenerationwebui': 0,
744 'top_a_counter_textgenerationwebui': 0,747 'top_a_counter_textgenerationwebui': 0,
745 'mirostat_mode_textgenerationwebui': 0,748 'mirostat_mode_textgenerationwebui': 0,
@@ -986,6 +989,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
986 let logprobs = null;989 let logprobs = null;
987 const swipes = [];990 const swipes = [];
988 const toolCalls = [];991 const toolCalls = [];
992 const state = { reasoning: '' };
989 while (true) {993 while (true) {
990 const { done, value } = await reader.read();994 const { done, value } = await reader.read();
991 if (done) return;995 if (done) return;
@@ -1002,9 +1006,10 @@ export async function generateTextGenWithStreaming(generate_data, signal) {
1002 const newText = data?.choices?.[0]?.text || data?.content || '';1006 const newText = data?.choices?.[0]?.text || data?.content || '';
1003 text += newText;1007 text += newText;
1004 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);1008 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
1009 state.reasoning += data?.choices?.[0]?.reasoning ?? '';
1005 }1010 }
10061011
1007 yield { text, swipes, logprobs, toolCalls };1012 yield { text, swipes, logprobs, toolCalls, state };
1008 }1013 }
1009 };1014 };
1010}1015}
@@ -1265,6 +1270,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1265 'truncation_length': max_context,1270 'truncation_length': max_context,
1266 'ban_eos_token': settings.ban_eos_token,1271 'ban_eos_token': settings.ban_eos_token,
1267 'skip_special_tokens': settings.skip_special_tokens,1272 'skip_special_tokens': settings.skip_special_tokens,
1273 'include_reasoning': settings.include_reasoning,
1268 'top_a': settings.top_a,1274 'top_a': settings.top_a,
1269 'tfs': settings.tfs,1275 'tfs': settings.tfs,
1270 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,1276 'epsilon_cutoff': [OOBA, MANCER].includes(settings.type) ? settings.epsilon_cutoff : undefined,
public/scripts/tokenizers.js+3 -0
@@ -679,6 +679,9 @@ export function getTokenizerModel() {
679 }679 }
680680
681 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {681 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
682 if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
683 return deepseekTokenizer;
684 }
682 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {685 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
683 return llama3Tokenizer;686 return llama3Tokenizer;
684 }687 }
public/scripts/utils.js+7 -7
@@ -1733,17 +1733,17 @@ export function hasAnimation(control) {
17331733
1734/**1734/**
1735 * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.1735 * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
1736 *1736 * The action will be executed after the animation ends or after the timeout, whichever comes first.
1737 * @param {HTMLElement} control - The control element to listen for animation end event1737 * @param {HTMLElement} control - The control element to listen for animation end event
1738 * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends1738 * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
1739 * @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback
1739 */1740 */
1740export function runAfterAnimation(control, callback) {1741export function runAfterAnimation(control, callback, timeout = 500) {
1741 if (hasAnimation(control)) {1742 if (hasAnimation(control)) {
1742 const onAnimationEnd = () => {1743 Promise.race([
1743 control.removeEventListener('animationend', onAnimationEnd);1744 new Promise((r) => setTimeout(r, timeout)), // Fallback timeout
1744 callback(control);1745 new Promise((r) => control.addEventListener('animationend', r, { once: true })),
1745 };1746 ]).finally(() => callback(control));
1746 control.addEventListener('animationend', onAnimationEnd);
1747 } else {1747 } else {
1748 callback(control);1748 callback(control);
1749 }1749 }
public/scripts/variables.js+4 -4
@@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1919
20const MAX_LOOPS = 100;20const MAX_LOOPS = 100;
2121
22function getLocalVariable(name, args = {}) {22export function getLocalVariable(name, args = {}) {
23 if (!chat_metadata.variables) {23 if (!chat_metadata.variables) {
24 chat_metadata.variables = {};24 chat_metadata.variables = {};
25 }25 }
@@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) {
45 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);45 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
46}46}
4747
48function setLocalVariable(name, value, args = {}) {48export function setLocalVariable(name, value, args = {}) {
49 if (!name) {49 if (!name) {
50 throw new Error('Variable name cannot be empty or undefined.');50 throw new Error('Variable name cannot be empty or undefined.');
51 }51 }
@@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) {
80 return value;80 return value;
81}81}
8282
83function getGlobalVariable(name, args = {}) {83export function getGlobalVariable(name, args = {}) {
84 let globalVariable = extension_settings.variables.global[args.key ?? name];84 let globalVariable = extension_settings.variables.global[args.key ?? name];
85 if (args.index !== undefined) {85 if (args.index !== undefined) {
86 try {86 try {
@@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) {
102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
103}103}
104104
105function setGlobalVariable(name, value, args = {}) {105export function setGlobalVariable(name, value, args = {}) {
106 if (!name) {106 if (!name) {
107 throw new Error('Variable name cannot be empty or undefined.');107 throw new Error('Variable name cannot be empty or undefined.');
108 }108 }
public/style.css+111 -21
@@ -292,36 +292,44 @@ input[type='checkbox']:focus-visible {
292 filter: grayscale(25%);292 filter: grayscale(25%);
293}293}
294294
295.mes_text table {295.mes_text table,
296.mes_reasoning table {
296 border-spacing: 0;297 border-spacing: 0;
297 border-collapse: collapse;298 border-collapse: collapse;
298 margin-bottom: 10px;299 margin-bottom: 10px;
299}300}
300301
301.mes_text td,302.mes_text td,
302.mes_text th {303.mes_text th,
304.mes_reasoning td,
305.mes_reasoning th {
303 border: 1px solid;306 border: 1px solid;
304 border-collapse: collapse;307 border-collapse: collapse;
305 padding: 0.25em;308 padding: 0.25em;
306}309}
307310
308.mes_text p {311.mes_text p,
312.mes_reasoning p {
309 margin-top: 0;313 margin-top: 0;
310 margin-bottom: 10px;314 margin-bottom: 10px;
311}315}
312316
313.mes_text li tt {317.mes_text li tt,
318.mes_reasoning li tt {
314 display: inline-block;319 display: inline-block;
315}320}
316321
317.mes_text ol,322.mes_text ol,
318.mes_text ul {323.mes_text ul,
324.mes_reasoning ol,
325.mes_reasoning ul {
319 margin-top: 5px;326 margin-top: 5px;
320 margin-bottom: 5px;327 margin-bottom: 5px;
321}328}
322329
323.mes_text br,330.mes_text br,
324.mes_bias br {331.mes_bias br,
332.mes_reasoning br {
325 content: ' ';333 content: ' ';
326}334}
327335
@@ -332,25 +340,88 @@ input[type='checkbox']:focus-visible {
332 color: var(--SmartThemeQuoteColor);340 color: var(--SmartThemeQuoteColor);
333}341}
334342
343.mes_reasoning {
344 display: block;
345 border: 1px solid var(--SmartThemeBorderColor);
346 background-color: var(--black30a);
347 border-radius: 5px;
348 padding: 5px;
349 margin: 5px 0;
350 overflow-y: auto;
351}
352
353.mes_reasoning_summary {
354 cursor: pointer;
355 position: relative;
356 margin: 2px;
357}
358
359@supports not selector(:has(*)) {
360 .mes_reasoning_details {
361 display: none !important;
362 }
363}
364
365.mes_bias:empty,
366.mes_reasoning:empty,
367.mes_reasoning_details:has(.mes_reasoning:empty),
368.mes_block:has(.edit_textarea) .mes_reasoning_details,
369.mes_reasoning_details:not([open]) .mes_reasoning_actions,
370.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
371.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done,
372.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel,
373.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) {
374 display: none;
375}
376
377.mes_reasoning_actions {
378 position: absolute;
379 right: 0;
380 top: 0;
381
382 display: flex;
383 gap: 4px;
384 flex-wrap: nowrap;
385 justify-content: flex-end;
386 transition: all 200ms;
387 overflow-x: hidden;
388 padding: 1px;
389}
390
391.mes_reasoning_summary>span {
392 margin-left: 0.5em;
393}
394
335.mes_text i,395.mes_text i,
336.mes_text em {396.mes_text em,
397.mes_reasoning i,
398.mes_reasoning em {
337 color: var(--SmartThemeEmColor);399 color: var(--SmartThemeEmColor);
338}400}
339401
340.mes_text u {402.mes_text q i,
403.mes_text q em {
404 color: inherit;
405}
406
407.mes_text u,
408.mes_reasoning u {
341 color: var(--SmartThemeUnderlineColor);409 color: var(--SmartThemeUnderlineColor);
342}410}
343411
344.mes_text q {412.mes_text q,
413.mes_reasoning q {
345 color: var(--SmartThemeQuoteColor);414 color: var(--SmartThemeQuoteColor);
346}415}
347416
348.mes_text font[color] em,417.mes_text font[color] em,
349.mes_text font[color] i {418.mes_text font[color] i,
350 color: inherit;419.mes_text font[color] u,
351}420.mes_text font[color] q,
352421.mes_reasoning font[color] em,
353.mes_text font[color] q {422.mes_reasoning font[color] i,
423.mes_reasoning font[color] u,
424.mes_reasoning font[color] q {
354 color: inherit;425 color: inherit;
355}426}
356427
@@ -358,7 +429,8 @@ input[type='checkbox']:focus-visible {
358 display: block;429 display: block;
359}430}
360431
361.mes_text blockquote {432.mes_text blockquote,
433.mes_reasoning blockquote {
362 border-left: 3px solid var(--SmartThemeQuoteColor);434 border-left: 3px solid var(--SmartThemeQuoteColor);
363 padding-left: 10px;435 padding-left: 10px;
364 background-color: var(--black30a);436 background-color: var(--black30a);
@@ -368,18 +440,24 @@ input[type='checkbox']:focus-visible {
368.mes_text strong em,440.mes_text strong em,
369.mes_text strong,441.mes_text strong,
370.mes_text h2,442.mes_text h2,
371.mes_text h1 {443.mes_text h1,
444.mes_reasoning strong em,
445.mes_reasoning strong,
446.mes_reasoning h2,
447.mes_reasoning h1 {
372 font-weight: bold;448 font-weight: bold;
373}449}
374450
375.mes_text pre code {451.mes_text pre code,
452.mes_reasoning pre code {
376 position: relative;453 position: relative;
377 display: block;454 display: block;
378 overflow-x: auto;455 overflow-x: auto;
379 padding: 1em;456 padding: 1em;
380}457}
381458
382.mes_text img:not(.mes_img) {459.mes_text img:not(.mes_img),
460.mes_reasoning img:not(.mes_img) {
383 max-width: 100%;461 max-width: 100%;
384 max-height: var(--doc-height);462 max-height: var(--doc-height);
385}463}
@@ -1022,6 +1100,11 @@ body .panelControlBar {
1022 /*only affects bubblechat to make it sit nicely at the bottom*/1100 /*only affects bubblechat to make it sit nicely at the bottom*/
1023}1101}
10241102
1103.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) {
1104 margin-bottom: 30px;
1105}
1106
1107.last_mes .mes_reasoning,
1025.last_mes .mes_text {1108.last_mes .mes_text {
1026 padding-right: 30px;1109 padding-right: 30px;
1027}1110}
@@ -1235,14 +1318,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1235 overflow-y: clip;1318 overflow-y: clip;
1236}1319}
12371320
1238.mes_text {1321.mes_text,
1322.mes_reasoning {
1239 font-weight: 500;1323 font-weight: 500;
1240 line-height: calc(var(--mainFontSize) + .5rem);1324 line-height: calc(var(--mainFontSize) + .5rem);
1325 max-width: 100%;
1326 overflow-wrap: anywhere;
1327}
1328
1329.mes_text {
1241 padding-left: 0;1330 padding-left: 0;
1242 padding-top: 5px;1331 padding-top: 5px;
1243 padding-bottom: 5px;1332 padding-bottom: 5px;
1244 max-width: 100%;
1245 overflow-wrap: anywhere;
1246}1333}
12471334
1248br {1335br {
@@ -4150,10 +4237,12 @@ input[type="range"]::-webkit-slider-thumb {
4150 align-items: center;4237 align-items: center;
4151}4238}
41524239
4240.mes_reasoning_edit_cancel,
4153.mes_edit_cancel.menu_button {4241.mes_edit_cancel.menu_button {
4154 background-color: var(--crimson70a);4242 background-color: var(--crimson70a);
4155}4243}
41564244
4245.mes_reasoning_edit_done,
4157.mes_edit_done.menu_button {4246.mes_edit_done.menu_button {
4158 background-color: var(--okGreen70a);4247 background-color: var(--okGreen70a);
4159}4248}
@@ -4162,6 +4251,7 @@ input[type="range"]::-webkit-slider-thumb {
4162 opacity: 1;4251 opacity: 1;
4163}4252}
41644253
4254.reasoning_edit_textarea,
4165.edit_textarea {4255.edit_textarea {
4166 padding: 5px;4256 padding: 5px;
4167 margin: 0;4257 margin: 0;
src/constants.js+1 -0
@@ -369,6 +369,7 @@ export const OPENROUTER_KEYS = [
369 'prompt',369 'prompt',
370 'stop',370 'stop',
371 'provider',371 'provider',
372 'include_reasoning',
372];373];
373374
374// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220375// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220
src/endpoints/backends/chat-completions.js+14 -8
@@ -288,7 +288,8 @@ async function sendMakerSuiteRequest(request, response) {
288288
289 const model = String(request.body.model);289 const model = String(request.body.model);
290 const stream = Boolean(request.body.stream);290 const stream = Boolean(request.body.stream);
291 const showThoughts = Boolean(request.body.show_thoughts);291 const showThoughts = Boolean(request.body.include_reasoning);
292 const isThinking = model.includes('thinking');
292293
293 const generationConfig = {294 const generationConfig = {
294 stopSequences: request.body.stop,295 stopSequences: request.body.stop,
@@ -329,6 +330,12 @@ async function sendMakerSuiteRequest(request, response) {
329 body.systemInstruction = prompt.system_instruction;330 body.systemInstruction = prompt.system_instruction;
330 }331 }
331332
333 if (isThinking && showThoughts) {
334 generationConfig.thinkingConfig = {
335 includeThoughts: true,
336 };
337 }
338
332 return body;339 return body;
333 }340 }
334341
@@ -342,7 +349,6 @@ async function sendMakerSuiteRequest(request, response) {
342 controller.abort();349 controller.abort();
343 });350 });
344351
345 const isThinking = model.includes('thinking');
346 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';352 const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
347 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');353 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
348354
@@ -387,11 +393,7 @@ async function sendMakerSuiteRequest(request, response) {
387 const responseContent = candidates[0].content ?? candidates[0].output;393 const responseContent = candidates[0].content ?? candidates[0].output;
388 console.log('Google AI Studio response:', responseContent);394 console.log('Google AI Studio response:', responseContent);
389395
390 if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {396 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
391 responseContent.parts = responseContent.parts.filter(part => !part.thought);
392 }
393
394 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n');
395 if (!responseText) {397 if (!responseText) {
396 let message = 'Google AI Studio Candidate text empty';398 let message = 'Google AI Studio Candidate text empty';
397 console.log(message, generateResponseJson);399 console.log(message, generateResponseJson);
@@ -399,7 +401,7 @@ async function sendMakerSuiteRequest(request, response) {
399 }401 }
400402
401 // Wrap it back to OAI format403 // Wrap it back to OAI format
402 const reply = { choices: [{ 'message': { 'content': responseText } }] };404 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
403 return response.send(reply);405 return response.send(reply);
404 }406 }
405 } catch (error) {407 } catch (error) {
@@ -996,6 +998,10 @@ router.post('/generate', jsonParser, function (request, response) {
996 bodyParams['route'] = 'fallback';998 bodyParams['route'] = 'fallback';
997 }999 }
9981000
1001 if (request.body.include_reasoning) {
1002 bodyParams['include_reasoning'] = true;
1003 }
1004
999 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);1005 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
1000 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {1006 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
1001 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);1007 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
src/endpoints/search.js+14 -3
@@ -162,7 +162,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
162162
163router.post('/searxng', jsonParser, async (request, response) => {163router.post('/searxng', jsonParser, async (request, response) => {
164 try {164 try {
165 const { baseUrl, query, preferences } = request.body;165 const { baseUrl, query, preferences, categories } = request.body;
166166
167 if (!baseUrl || !query) {167 if (!baseUrl || !query) {
168 console.log('Missing required parameters for /searxng');168 console.log('Missing required parameters for /searxng');
@@ -193,6 +193,9 @@ router.post('/searxng', jsonParser, async (request, response) => {
193 if (preferences) {193 if (preferences) {
194 searchParams.append('preferences', preferences);194 searchParams.append('preferences', preferences);
195 }195 }
196 if (categories) {
197 searchParams.append('categories', categories);
198 }
196 searchUrl.search = searchParams.toString();199 searchUrl.search = searchParams.toString();
197200
198 const searchResult = await fetch(searchUrl, { headers: visitHeaders });201 const searchResult = await fetch(searchUrl, { headers: visitHeaders });
@@ -220,7 +223,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
220 return response.sendStatus(400);223 return response.sendStatus(400);
221 }224 }
222225
223 const { query } = request.body;226 const { query, include_images } = request.body;
224227
225 const body = {228 const body = {
226 query: query,229 query: query,
@@ -229,7 +232,7 @@ router.post('/tavily', jsonParser, async (request, response) => {
229 topic: 'general',232 topic: 'general',
230 include_answer: true,233 include_answer: true,
231 include_raw_content: false,234 include_raw_content: false,
232 include_images: false,235 include_images: !!include_images,
233 include_image_descriptions: false,236 include_image_descriptions: false,
234 include_domains: [],237 include_domains: [],
235 max_results: 10,238 max_results: 10,
@@ -297,6 +300,7 @@ router.post('/koboldcpp', jsonParser, async (request, response) => {
297router.post('/visit', jsonParser, async (request, response) => {300router.post('/visit', jsonParser, async (request, response) => {
298 try {301 try {
299 const url = request.body.url;302 const url = request.body.url;
303 const html = Boolean(request.body.html ?? true);
300304
301 if (!url) {305 if (!url) {
302 console.log('No url provided for /visit');306 console.log('No url provided for /visit');
@@ -340,6 +344,8 @@ router.post('/visit', jsonParser, async (request, response) => {
340 }344 }
341345
342 const contentType = String(result.headers.get('content-type'));346 const contentType = String(result.headers.get('content-type'));
347
348 if (html) {
343 if (!contentType.includes('text/html')) {349 if (!contentType.includes('text/html')) {
344 console.log(`Visit failed, content-type is ${contentType}, expected text/html`);350 console.log(`Visit failed, content-type is ${contentType}, expected text/html`);
345 return response.sendStatus(500);351 return response.sendStatus(500);
@@ -347,6 +353,11 @@ router.post('/visit', jsonParser, async (request, response) => {
347353
348 const text = await result.text();354 const text = await result.text();
349 return response.send(text);355 return response.send(text);
356 }
357
358 response.setHeader('Content-Type', contentType);
359 const buffer = await result.arrayBuffer();
360 return response.send(Buffer.from(buffer));
350 } catch (error) {361 } catch (error) {
351 console.log(error);362 console.log(error);
352 return response.sendStatus(500);363 return response.sendStatus(500);
src/endpoints/stable-diffusion.js+1 -1
@@ -533,6 +533,7 @@ comfy.post('/delete-workflow', jsonParser, async (request, response) => {
533533
534comfy.post('/generate', jsonParser, async (request, response) => {534comfy.post('/generate', jsonParser, async (request, response) => {
535 try {535 try {
536 let item;
536 const url = new URL(urlJoin(request.body.url, '/prompt'));537 const url = new URL(urlJoin(request.body.url, '/prompt'));
537538
538 const controller = new AbortController();539 const controller = new AbortController();
@@ -557,7 +558,6 @@ comfy.post('/generate', jsonParser, async (request, response) => {
557 /** @type {any} */558 /** @type {any} */
558 const data = await promptResult.json();559 const data = await promptResult.json();
559 const id = data.prompt_id;560 const id = data.prompt_id;
560 let item;
561 const historyUrl = new URL(urlJoin(request.body.url, '/history'));561 const historyUrl = new URL(urlJoin(request.body.url, '/history'));
562 while (true) {562 while (true) {
563 const result = await fetch(historyUrl);563 const result = await fetch(historyUrl);
src/endpoints/tokenizers.js+1 -1
@@ -398,7 +398,7 @@ function getWebTokenizersChunks(tokenizer, ids) {
398 * @returns {string} Tokenizer model to use398 * @returns {string} Tokenizer model to use
399 */399 */
400export function getTokenizerModel(requestModel) {400export function getTokenizerModel(requestModel) {
401 if (requestModel.includes('o1-preview') || requestModel.includes('o1-mini')) {401 if (requestModel.includes('o1-preview') || requestModel.includes('o1-mini') || requestModel.includes('o3-mini')) {
402 return 'gpt-4o';402 return 'gpt-4o';
403 }403 }
404404