Merge pull request #3442 from SillyTavern/more-reasoning-ui Smaller improvements and fixes to the reasoning UI

5886bb6b3a9f150a45e0157ccc59404bc928fe9f

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

Signed
3 files changed, +91 -84Ignore whitespace
public/script.js+9 -78
@@ -225,7 +225,7 @@ import {
225 instruct_presets,225 instruct_presets,
226 selectContextPreset,226 selectContextPreset,
227} from './scripts/instruct-mode.js';227} from './scripts/instruct-mode.js';
228import { getCurrentLocale, initLocales, t } from './scripts/i18n.js';228import { initLocales, t } from './scripts/i18n.js';
229import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js';229import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js';
230import {230import {
231 user_avatar,231 user_avatar,
@@ -269,7 +269,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
269import { initBulkEdit } from './scripts/bulk-edit.js';269import { initBulkEdit } from './scripts/bulk-edit.js';
270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
271import { getContext } from './scripts/st-context.js';271import { getContext } from './scripts/st-context.js';
272import { initReasoning, PromptReasoning } from './scripts/reasoning.js';272import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI } from './scripts/reasoning.js';
273273
274// API OBJECT FOR EXTERNAL WIRING274// API OBJECT FOR EXTERNAL WIRING
275globalThis.SillyTavern = {275globalThis.SillyTavern = {
@@ -2275,11 +2275,15 @@ function getMessageFromTemplate({
2275 * Re-renders a message block with updated content.2275 * Re-renders a message block with updated content.
2276 * @param {number} messageId Message ID2276 * @param {number} messageId Message ID
2277 * @param {object} message Message object2277 * @param {object} message Message object
2278 * @param {object} [options={}] Optional arguments
2279 * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>)
2278 */2280 */
2279export function updateMessageBlock(messageId, message) {2281export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) {
2280 const messageElement = $(`#chat [mesid="${messageId}"]`);2282 const messageElement = $(`#chat [mesid="${messageId}"]`);
2281 const text = message?.extra?.display_text ?? message.mes;2283 if (rerenderMessage) {
2282 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));2284 const text = message?.extra?.display_text ?? message.mes;
2285 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2286 }
2283 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));2287 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
2284 messageElement.toggleClass('reasoning', !!message.extra?.reasoning);2288 messageElement.toggleClass('reasoning', !!message.extra?.reasoning);
2285 addCopyToCodeBlocks(messageElement);2289 addCopyToCodeBlocks(messageElement);
@@ -5764,56 +5768,6 @@ function extractMessageFromData(data) {
5764}5768}
57655769
5766/**5770/**
5767 * Extracts the reasoning from the response data.
5768 * @param {object} data Response data
5769 * @returns {string} Extracted reasoning
5770 */
5771function extractReasoningFromData(data) {
5772 switch (main_api) {
5773 case 'textgenerationwebui':
5774 switch (textgen_settings.type) {
5775 case textgen_types.OPENROUTER:
5776 return data?.choices?.[0]?.reasoning ?? '';
5777 }
5778 break;
5779
5780 case 'openai':
5781 if (!oai_settings.show_thoughts) break;
5782
5783 switch (oai_settings.chat_completion_source) {
5784 case chat_completion_sources.DEEPSEEK:
5785 return data?.choices?.[0]?.message?.reasoning_content ?? '';
5786 case chat_completion_sources.OPENROUTER:
5787 return data?.choices?.[0]?.message?.reasoning ?? '';
5788 case chat_completion_sources.MAKERSUITE:
5789 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5790 }
5791 break;
5792 }
5793
5794 return '';
5795}
5796
5797/**
5798 * Updates the Reasoning controls
5799 * @param {HTMLElement} element The element to update
5800 * @param {number?} duration The duration of the reasoning in milliseconds
5801 * @param {object} [options={}] Options for the function
5802 * @param {boolean} [options.forceEnd=false] If true, there will be no "Thinking..." when no duration exists
5803 */
5804function updateReasoningTimeUI(element, duration, { forceEnd = false } = {}) {
5805 if (duration) {
5806 const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 9 });
5807 element.textContent = t`Thought for ${durationStr}`;
5808 } else if (forceEnd) {
5809 element.textContent = t`Thought for some time`;
5810 } else {
5811 element.textContent = t`Thinking...`;
5812 }
5813}
5814
5815
5816/**
5817 * Extracts multiswipe swipes from the response data.5771 * Extracts multiswipe swipes from the response data.
5818 * @param {Object} data Response data5772 * @param {Object} data Response data
5819 * @param {string} type Type of generation5773 * @param {string} type Type of generation
@@ -10872,18 +10826,6 @@ jQuery(async function () {
10872 }10826 }
10873 });10827 });
1087410828
10875 $(document).on('click', '.mes_reasoning_header', function () {
10876 // If we are in message edit mode and reasoning area is closed, a click opens and edits it
10877 const mes = $(this).closest('.mes');
10878 const mesEditArea = mes.find('#curEditTextarea');
10879 if (mesEditArea.length) {
10880 const summary = $(mes).find('.mes_reasoning_summary');
10881 if (!summary.attr('open')) {
10882 summary.find('.mes_reasoning_edit').trigger('click');
10883 }
10884 }
10885 });
10886
10887 $(document).on('input', '#curEditTextarea', function () {10829 $(document).on('input', '#curEditTextarea', function () {
10888 if (power_user.auto_save_msg_edits === true) {10830 if (power_user.auto_save_msg_edits === true) {
10889 messageEditAuto($(this));10831 messageEditAuto($(this));
@@ -11438,17 +11380,6 @@ jQuery(async function () {
11438 }11380 }
11439 });11381 });
1144011382
11441 $(document).on('click', '.mes_reasoning_summary', function () {
11442 // If you toggle summary header while editing reasoning, yup - we just cancel it
11443 $(this).closest('.mes').find('.mes_reasoning_edit_cancel:visible').trigger('click');
11444 });
11445
11446 $(document).on('click', '.mes_reasoning_details', function (e) {
11447 if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
11448 e.preventDefault();
11449 }
11450 });
11451
11452 $(document).keyup(function (e) {11383 $(document).keyup(function (e) {
11453 if (e.key === 'Escape') {11384 if (e.key === 'Escape') {
11454 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;11385 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
public/scripts/reasoning.js+81 -5
@@ -1,13 +1,18 @@
1import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';1import {
2 moment,
3} from '../lib.js';
4import { chat, closeMessageEditor, event_types, eventSource, main_api, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2import { getRegexedString, regex_placement } from './extensions/regex/engine.js';5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
3import { t } from './i18n.js';6import { getCurrentLocale, t } from './i18n.js';
4import { MacrosParser } from './macros.js';7import { MacrosParser } from './macros.js';
8import { chat_completion_sources, oai_settings } from './openai.js';
5import { Popup } from './popup.js';9import { Popup } from './popup.js';
6import { power_user } from './power-user.js';10import { power_user } from './power-user.js';
7import { SlashCommand } from './slash-commands/SlashCommand.js';11import { SlashCommand } from './slash-commands/SlashCommand.js';
8import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';12import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
9import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';13import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
10import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';14import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
15import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
11import { copyText, escapeRegex, isFalseBoolean } from './utils.js';16import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
1217
13/**18/**
@@ -35,6 +40,56 @@ function toggleReasoningAutoExpand() {
35}40}
3641
37/**42/**
43 * Extracts the reasoning from the response data.
44 * @param {object} data Response data
45 * @returns {string} Extracted reasoning
46 */
47export function extractReasoningFromData(data) {
48 switch (main_api) {
49 case 'textgenerationwebui':
50 switch (textgenerationwebui_settings.type) {
51 case textgen_types.OPENROUTER:
52 return data?.choices?.[0]?.reasoning ?? '';
53 }
54 break;
55
56 case 'openai':
57 if (!oai_settings.show_thoughts) break;
58
59 switch (oai_settings.chat_completion_source) {
60 case chat_completion_sources.DEEPSEEK:
61 return data?.choices?.[0]?.message?.reasoning_content ?? '';
62 case chat_completion_sources.OPENROUTER:
63 return data?.choices?.[0]?.message?.reasoning ?? '';
64 case chat_completion_sources.MAKERSUITE:
65 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
66 }
67 break;
68 }
69
70 return '';
71}
72
73/**
74 * Updates the Reasoning controls
75 * @param {HTMLElement} element The element to update
76 * @param {number?} duration The duration of the reasoning in milliseconds
77 * @param {object} [options={}] Options for the function
78 * @param {boolean} [options.forceEnd=false] If true, there will be no "Thinking..." when no duration exists
79 */
80export function updateReasoningTimeUI(element, duration, { forceEnd = false } = {}) {
81 if (duration) {
82 const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
83 const secondsStr = moment.duration(duration).asSeconds();
84 element.innerHTML = t`Thought for <span title="${secondsStr} seconds">${durationStr}</span>`;
85 } else if (forceEnd) {
86 element.textContent = t`Thought for some time`;
87 } else {
88 element.textContent = t`Thinking...`;
89 }
90}
91
92/**
38 * Helper class for adding reasoning to messages.93 * Helper class for adding reasoning to messages.
39 * Keeps track of the number of reasoning additions.94 * Keeps track of the number of reasoning additions.
40 */95 */
@@ -247,6 +302,24 @@ function registerReasoningMacros() {
247}302}
248303
249function setReasoningEventHandlers() {304function setReasoningEventHandlers() {
305 $(document).on('click', '.mes_reasoning_details', function (e) {
306 if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
307 e.preventDefault();
308 }
309 });
310
311 $(document).on('click', '.mes_reasoning_header', function () {
312 // If we are in message edit mode and reasoning area is closed, a click opens and edits it
313 const mes = $(this).closest('.mes');
314 const mesEditArea = mes.find('#curEditTextarea');
315 if (mesEditArea.length) {
316 const summary = $(mes).find('.mes_reasoning_summary');
317 if (!summary.attr('open')) {
318 summary.find('.mes_reasoning_edit').trigger('click');
319 }
320 }
321 });
322
250 $(document).on('click', '.mes_reasoning_copy', (e) => {323 $(document).on('click', '.mes_reasoning_copy', (e) => {
251 e.stopPropagation();324 e.stopPropagation();
252 e.preventDefault();325 e.preventDefault();
@@ -323,7 +396,7 @@ function setReasoningEventHandlers() {
323 });396 });
324397
325 $(document).on('click', '.mes_edit_add_reasoning', async function () {398 $(document).on('click', '.mes_edit_add_reasoning', async function () {
326 const { message, messageId } = getMessageFromJquery(this);399 const { message, messageId, messageBlock } = getMessageFromJquery(this);
327 if (!message?.extra) {400 if (!message?.extra) {
328 return;401 return;
329 }402 }
@@ -334,7 +407,8 @@ function setReasoningEventHandlers() {
334 }407 }
335408
336 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;409 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
337 updateMessageBlock(messageId, message);410 updateMessageBlock(messageId, message, { rerenderMessage: false });
411 messageBlock.find('.mes_reasoning_edit').trigger('click');
338 await saveChatConditional();412 await saveChatConditional();
339 });413 });
340414
@@ -348,13 +422,15 @@ function setReasoningEventHandlers() {
348 return;422 return;
349 }423 }
350424
351 const { message, messageId } = getMessageFromJquery(this);425 const { message, messageId, messageBlock } = getMessageFromJquery(this);
352 if (!message?.extra) {426 if (!message?.extra) {
353 return;427 return;
354 }428 }
355 message.extra.reasoning = '';429 message.extra.reasoning = '';
356 await saveChatConditional();430 await saveChatConditional();
357 updateMessageBlock(messageId, message);431 updateMessageBlock(messageId, message);
432 const textarea = messageBlock.find('.reasoning_edit_textarea');
433 textarea.remove();
358 });434 });
359435
360 $(document).on('pointerup', '.mes_reasoning_copy', async function () {436 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
public/style.css+1 -1
@@ -388,7 +388,7 @@ input[type='checkbox']:focus-visible {
388 margin: 0.5em 2px;388 margin: 0.5em 2px;
389 padding: 7px 14px;389 padding: 7px 14px;
390 padding-right: calc(0.7em + 14px);390 padding-right: calc(0.7em + 14px);
391 border-radius: 10px;391 border-radius: 5px;
392 background-color: var(--grey30);392 background-color: var(--grey30);
393 font-size: calc(var(--mainFontSize) * 0.9);393 font-size: calc(var(--mainFontSize) * 0.9);
394 align-items: baseline;394 align-items: baseline;