Merge pull request #3442 from SillyTavern/more-reasoning-ui Smaller improvements and fixes to the reasoning UI
Signed| @@ -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'; |
| 228 | import { getCurrentLocale, initLocales, t } from './scripts/i18n.js'; | 228 | import { initLocales, t } from './scripts/i18n.js'; |
| 229 | import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js'; | 229 | import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js'; |
| 230 | import { | 230 | import { |
| 231 | user_avatar, | 231 | user_avatar, |
| @@ -269,7 +269,7 @@ import { initSettingsSearch } from './scripts/setting-search.js'; | |||
| 269 | import { initBulkEdit } from './scripts/bulk-edit.js'; | 269 | import { initBulkEdit } from './scripts/bulk-edit.js'; |
| 270 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; | 270 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; |
| 271 | import { getContext } from './scripts/st-context.js'; | 271 | import { getContext } from './scripts/st-context.js'; |
| 272 | import { initReasoning, PromptReasoning } from './scripts/reasoning.js'; | 272 | import { extractReasoningFromData, initReasoning, PromptReasoning, updateReasoningTimeUI } from './scripts/reasoning.js'; |
| 273 | 273 | ||
| 274 | // API OBJECT FOR EXTERNAL WIRING | 274 | // API OBJECT FOR EXTERNAL WIRING |
| 275 | globalThis.SillyTavern = { | 275 | globalThis.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 ID | 2276 | * @param {number} messageId Message ID |
| 2277 | * @param {object} message Message object | 2277 | * @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 | */ |
| 2279 | export function updateMessageBlock(messageId, message) { | 2281 | export 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 | } |
| 5765 | 5769 | ||
| 5766 | /** | 5770 | /** |
| 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 | /** | ||
| 5817 | * Extracts multiswipe swipes from the response data. | 5771 | * Extracts multiswipe swipes from the response data. |
| 5818 | * @param {Object} data Response data | 5772 | * @param {Object} data Response data |
| 5819 | * @param {string} type Type of generation | 5773 | * @param {string} type Type of generation |
| @@ -10872,18 +10826,6 @@ jQuery(async function () { | |||
| 10872 | } | 10826 | } |
| 10873 | }); | 10827 | }); |
| 10874 | 10828 | ||
| 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 | }); |
| 11440 | 11382 | ||
| 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; |
| @@ -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'; | ||
| 2 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; | 5 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 3 | import { t } from './i18n.js'; | 6 | import { getCurrentLocale, t } from './i18n.js'; |
| 4 | import { MacrosParser } from './macros.js'; | 7 | import { MacrosParser } from './macros.js'; |
| 8 | import { chat_completion_sources, oai_settings } from './openai.js'; | ||
| 5 | import { Popup } from './popup.js'; | 9 | import { Popup } from './popup.js'; |
| 6 | import { power_user } from './power-user.js'; | 10 | import { power_user } from './power-user.js'; |
| 7 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 11 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 8 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; | 12 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 9 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 13 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 10 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 14 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 15 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; | ||
| 11 | import { copyText, escapeRegex, isFalseBoolean } from './utils.js'; | 16 | import { copyText, escapeRegex, isFalseBoolean } from './utils.js'; |
| 12 | 17 | ||
| 13 | /** | 18 | /** |
| @@ -35,6 +40,56 @@ function toggleReasoningAutoExpand() { | |||
| 35 | } | 40 | } |
| 36 | 41 | ||
| 37 | /** | 42 | /** |
| 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 | /** | ||
| 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 | } |
| 248 | 303 | ||
| 249 | function setReasoningEventHandlers() { | 304 | function 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 | }); |
| 324 | 397 | ||
| 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 | } |
| 335 | 408 | ||
| 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 | }); |
| 340 | 414 | ||
| @@ -348,13 +422,15 @@ function setReasoningEventHandlers() { | |||
| 348 | return; | 422 | return; |
| 349 | } | 423 | } |
| 350 | 424 | ||
| 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 | }); |
| 359 | 435 | ||
| 360 | $(document).on('pointerup', '.mes_reasoning_copy', async function () { | 436 | $(document).on('pointerup', '.mes_reasoning_copy', async function () { |
| @@ -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; |