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 {
225225 instruct_presets,
226226 selectContextPreset,
227227} from './scripts/instruct-mode.js';
228228import { getCurrentLocale, initLocales, t } from './scripts/i18n.js';
229229import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js';
230230import {
231231 user_avatar,
@@ -269,7 +269,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
269269import { initBulkEdit } from './scripts/bulk-edit.js';
270270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
271271import { getContext } from './scripts/st-context.js';
272272import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI } from './scripts/reasoning.js';
273273
274274// API OBJECT FOR EXTERNAL WIRING
275275globalThis.SillyTavern = {
@@ -2275,11 +2275,15 @@ function getMessageFromTemplate({
22752275 * Re-renders a message block with updated content.
22762276 * @param {number} messageId Message ID
22772277 * @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>)
22782280 */
22792281export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) {
22802282 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+ }
22832287 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
22842288 messageElement.toggleClass('reasoning', !!message.extra?.reasoning);
22852289 addCopyToCodeBlocks(messageElement);
@@ -5764,56 +5768,6 @@ function extractMessageFromData(data) {
57645768}
57655769
57665770/**
5767- * Extracts the reasoning from the response data.
5768- * @param {object} data Response data
5769- * @returns {string} Extracted reasoning
5770- */
5771-function 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- */
5804-function 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-/**
58175771 * Extracts multiswipe swipes from the response data.
58185772 * @param {Object} data Response data
58195773 * @param {string} type Type of generation
@@ -10872,18 +10826,6 @@ jQuery(async function () {
1087210826 }
1087310827 });
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-
1088710829 $(document).on('input', '#curEditTextarea', function () {
1088810830 if (power_user.auto_save_msg_edits === true) {
1088910831 messageEditAuto($(this));
@@ -11438,17 +11380,6 @@ jQuery(async function () {
1143811380 }
1143911381 });
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-
1145211383 $(document).keyup(function (e) {
1145311384 if (e.key === 'Escape') {
1145411385 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
public/scripts/reasoning.js+81 -5
@@ -1,13 +1,18 @@
1-import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
1+import {
2+ moment,
3+} from '../lib.js';
4+import { chat, closeMessageEditor, event_types, eventSource, main_api, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
25import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
36import { getCurrentLocale, t } from './i18n.js';
47import { MacrosParser } from './macros.js';
8+import { chat_completion_sources, oai_settings } from './openai.js';
59import { Popup } from './popup.js';
610import { power_user } from './power-user.js';
711import { SlashCommand } from './slash-commands/SlashCommand.js';
812import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
913import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
1014import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
15+import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
1116import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
1217
1318/**
@@ -35,6 +40,56 @@ function toggleReasoningAutoExpand() {
3540}
3641
3742/**
43+ * Extracts the reasoning from the response data.
44+ * @param {object} data Response data
45+ * @returns {string} Extracted reasoning
46+ */
47+export 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+ */
80+export 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+/**
3893 * Helper class for adding reasoning to messages.
3994 * Keeps track of the number of reasoning additions.
4095 */
@@ -247,6 +302,24 @@ function registerReasoningMacros() {
247302}
248303
249304function 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+
250323 $(document).on('click', '.mes_reasoning_copy', (e) => {
251324 e.stopPropagation();
252325 e.preventDefault();
@@ -323,7 +396,7 @@ function setReasoningEventHandlers() {
323396 });
324397
325398 $(document).on('click', '.mes_edit_add_reasoning', async function () {
326399 const { message, messageId, messageBlock } = getMessageFromJquery(this);
327400 if (!message?.extra) {
328401 return;
329402 }
@@ -334,7 +407,8 @@ function setReasoningEventHandlers() {
334407 }
335408
336409 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
337410 updateMessageBlock(messageId, message, { rerenderMessage: false });
411+ messageBlock.find('.mes_reasoning_edit').trigger('click');
338412 await saveChatConditional();
339413 });
340414
@@ -348,13 +422,15 @@ function setReasoningEventHandlers() {
348422 return;
349423 }
350424
351425 const { message, messageId, messageBlock } = getMessageFromJquery(this);
352426 if (!message?.extra) {
353427 return;
354428 }
355429 message.extra.reasoning = '';
356430 await saveChatConditional();
357431 updateMessageBlock(messageId, message);
432+ const textarea = messageBlock.find('.reasoning_edit_textarea');
433+ textarea.remove();
358434 });
359435
360436 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
public/style.css+1 -1
@@ -388,7 +388,7 @@ input[type='checkbox']:focus-visible {
388388 margin: 0.5em 2px;
389389 padding: 7px 14px;
390390 padding-right: calc(0.7em + 14px);
391391 border-radius: 10px5px;
392392 background-color: var(--grey30);
393393 font-size: calc(var(--mainFontSize) * 0.9);
394394 align-items: baseline;