import { moment, } from '../lib.js'; import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; import { getCurrentLocale, t, translate } from './i18n.js'; import { MacrosParser } from './macros.js'; import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js'; import { Popup } from './popup.js'; import { power_user } from './power-user.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js'; /** * Enum representing the type of the reasoning for a message (where it came from) * @enum {string} * @readonly */ export const ReasoningType = { Model: 'model', Parsed: 'parsed', Manual: 'manual', Edited: 'edited', }; /** * Gets a message from a jQuery element. * @param {Element} element * @returns {{messageId: number, message: object, messageBlock: JQuery}} */ function getMessageFromJquery(element) { const messageBlock = $(element).closest('.mes'); const messageId = Number(messageBlock.attr('mesid')); const message = chat[messageId]; return { messageId: messageId, message, messageBlock }; } /** * Toggles the auto-expand state of reasoning blocks. */ function toggleReasoningAutoExpand() { const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details'); reasoningBlocks.forEach((block) => { if (block instanceof HTMLDetailsElement) { block.open = power_user.reasoning.auto_expand; } }); } /** * Extracts the reasoning from the response data. * @param {object} data Response data * @returns {string} Extracted reasoning */ export function extractReasoningFromData(data) { switch (main_api) { case 'textgenerationwebui': switch (textgenerationwebui_settings.type) { case textgen_types.OPENROUTER: return data?.choices?.[0]?.reasoning ?? ''; } break; case 'openai': if (!oai_settings.show_thoughts) break; switch (oai_settings.chat_completion_source) { case chat_completion_sources.DEEPSEEK: return data?.choices?.[0]?.message?.reasoning_content ?? ''; case chat_completion_sources.OPENROUTER: return data?.choices?.[0]?.message?.reasoning ?? ''; case chat_completion_sources.MAKERSUITE: return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? ''; case chat_completion_sources.CUSTOM: { return data?.choices?.[0]?.message?.reasoning_content ?? data?.choices?.[0]?.message?.reasoning ?? ''; } } break; } return ''; } /** * Check if the model supports reasoning, but does not send back the reasoning * @returns {boolean} True if the model supports reasoning */ export function isHiddenReasoningModel() { if (main_api !== 'openai') { return false; } /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */ /** @type {Record.} */ const FUNCS = { equals: (currentModel, supportedModel) => currentModel === supportedModel, startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel), }; /** @type {{ name: string; func: MatchingFunc; }[]} */ const hiddenReasoningModels = [ { name: 'o1', func: FUNCS.startsWith }, { name: 'o3', func: FUNCS.startsWith }, { name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith }, { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith }, ]; const model = getChatCompletionModel() || ''; const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name)); return isHidden; } /** * Updates the Reasoning UI for a specific message * @param {number|JQuery|HTMLElement} messageIdOrElement The message ID or the message element * @param {Object} [options={}] - Optional arguments * @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping) */ export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) { const handler = new ReasoningHandler(); handler.initHandleMessage(messageIdOrElement, { reset }); } /** * Enum for representing the state of reasoning * @enum {string} * @readonly */ export const ReasoningState = { None: 'none', Thinking: 'thinking', Done: 'done', Hidden: 'hidden', }; /** * Handles reasoning-specific logic and DOM updates for messages. * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates. */ export class ReasoningHandler { /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */ #isHiddenReasoningModel; /** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */ #isParsingReasoning = false; /** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */ #parsingReasoningMesStartIndex = null; /** * @param {Date?} [timeStarted=null] - When the generation started */ constructor(timeStarted = null) { /** @type {ReasoningState} The current state of the reasoning process */ this.state = ReasoningState.None; /** @type {ReasoningType?} The type of the reasoning (where it came from) */ this.type = null; /** @type {string} The reasoning output */ this.reasoning = ''; /** @type {Date} When the reasoning started */ this.startTime = null; /** @type {Date} When the reasoning ended */ this.endTime = null; /** @type {Date} Initial starting time of the generation */ this.initialTime = timeStarted ?? new Date(); this.#isHiddenReasoningModel = isHiddenReasoningModel(); // Cached DOM elements for reasoning /** @type {HTMLElement} Main message DOM element `.mes` */ this.messageDom = null; /** @type {HTMLDetailsElement} Reasoning details DOM element `.mes_reasoning_details` */ this.messageReasoningDetailsDom = null; /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */ this.messageReasoningContentDom = null; /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */ this.messageReasoningHeaderDom = null; } /** * Initializes the reasoning handler for a specific message. * * Can be used to update the DOM elements or read other reasoning states. * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message. * The state will always be either done/hidden or none. * * @param {number|JQuery|HTMLElement} messageIdOrElement - The message ID or the message element * @param {Object} [options={}] - Optional arguments * @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping) */ initHandleMessage(messageIdOrElement, { reset = false } = {}) { /** @type {HTMLElement} */ const messageElement = typeof messageIdOrElement === 'number' ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`) : messageIdOrElement instanceof HTMLElement ? messageIdOrElement : $(messageIdOrElement)[0]; const messageId = Number(messageElement.getAttribute('mesid')); if (isNaN(messageId) || !chat[messageId]) return; if (!chat[messageId].extra) { chat[messageId].extra = {}; } const extra = chat[messageId].extra; if (extra.reasoning) { this.state = ReasoningState.Done; } else if (extra.reasoning_duration) { this.state = ReasoningState.Hidden; } this.type = extra?.reasoning_type; this.reasoning = extra?.reasoning ?? ''; if (this.state !== ReasoningState.None) { this.initialTime = new Date(chat[messageId].gen_started); this.startTime = this.initialTime; this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0)); } // Prefill main dom element, as message might not have been rendered yet this.messageDom = messageElement; // Make sure reset correctly clears all relevant states if (reset) { this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None; this.type = null; this.reasoning = ''; this.initialTime = new Date(); this.startTime = null; this.endTime = null; } this.updateDom(messageId); if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) { this.messageReasoningDetailsDom.open = true; } } /** * Gets the duration of the reasoning in milliseconds. * * @returns {number?} The duration in milliseconds, or null if the start or end time is not set */ getDuration() { if (this.startTime && this.endTime) { return this.endTime.getTime() - this.startTime.getTime(); } return null; } /** * Updates the reasoning text/string for a message. * * @param {number} messageId - The ID of the message to update * @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning * @param {Object} [options={}] - Optional arguments * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object * @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one * @returns {boolean} - Returns true if the reasoning was changed, otherwise false */ updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) { if (messageId == -1 || !chat[messageId]) { return false; } reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning; reasoning = trimSpaces(reasoning); // Ensure the chat extra exists if (!chat[messageId].extra) { chat[messageId].extra = {}; } const extra = chat[messageId].extra; const reasoningChanged = extra.reasoning !== reasoning; this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING); this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model; if (persist) { // Build and save the reasoning data to message extras extra.reasoning = this.reasoning; extra.reasoning_duration = this.getDuration(); extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model; } return reasoningChanged; } /** * Handles processing of reasoning for a message. * * This is usually called by the message processor when a message is changed. * * @param {number} messageId - The ID of the message to process * @param {boolean} mesChanged - Whether the message has changed * @returns {Promise} */ async process(messageId, mesChanged) { mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged); if (!this.reasoning && !this.#isHiddenReasoningModel) return; // Ensure reasoning string is updated and regexes are applied correctly const reasoningChanged = this.updateReasoning(messageId, null, { persist: true }); if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) { this.state = ReasoningState.Thinking; this.startTime = this.initialTime; } if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) { this.endTime = new Date(); await this.finish(messageId); } } #autoParseReasoningFromMessage(messageId, mesChanged) { if (!power_user.reasoning.auto_parse) return; if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) return mesChanged; /** @type {{ mes: string, [key: string]: any}} */ const message = chat[messageId]; if (!message) return mesChanged; // If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it. if (this.#parsingReasoningMesStartIndex) { message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex)); return mesChanged; } if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) { // If streamed message starts with the opening, cut it out and put all inside reasoning if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) { this.#isParsingReasoning = true; // Manually set starting state here, as we might already have received the ending suffix this.state = ReasoningState.Thinking; this.startTime = this.startTime ?? this.initialTime; this.endTime = null; } } if (!this.#isParsingReasoning) return mesChanged; // If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block const originalMes = message.mes; this.reasoning = originalMes.slice(power_user.reasoning.prefix.length); message.mes = ''; // If the reasoning contains the ending suffix, we cut that off and continue as message streaming if (this.reasoning.includes(power_user.reasoning.suffix)) { this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix)); this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length; message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex)); this.#isParsingReasoning = false; } // Only return the original mesChanged value if we haven't cut off the complete message return message.mes.length ? mesChanged : false; } /** * Completes the reasoning process for a message. * * Records the finish time if it was not set during streaming and updates the reasoning state. * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly. * * @param {number} messageId - The ID of the message to complete reasoning for * @returns {Promise} */ async finish(messageId) { if (this.state === ReasoningState.None) return; // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming if (this.startTime !== null && this.endTime === null) { this.endTime = new Date(); } if (this.state === ReasoningState.Thinking) { this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done; this.updateReasoning(messageId, null, { persist: true }); await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state); } this.updateDom(messageId); } /** * Updates the reasoning UI elements for a message. * * Toggles the CSS class, updates states, reasoning message, and duration. * * @param {number} messageId - The ID of the message to update */ updateDom(messageId) { this.#checkDomElements(messageId); // Main CSS class to show this message includes reasoning this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None); // Update states to the relevant DOM elements setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null); setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state); setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type); // Update the reasoning message const reasoning = trimSpaces(this.reasoning); const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); this.messageReasoningContentDom.innerHTML = displayReasoning; // Update tooltip for hidden reasoning edit /** @type {HTMLElement} */ const button = this.messageDom.querySelector('.mes_edit_add_reasoning'); button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`; // Make sure that hidden reasoning headers are collapsed by default, to not show a useless edit button if (this.state === ReasoningState.Hidden) { this.messageReasoningDetailsDom.open = false; } // Update the reasoning duration in the UI this.#updateReasoningTimeUI(); } /** * Finds and caches reasoning-related DOM elements for the given message. * * @param {number} messageId - The ID of the message to cache the DOM elements for */ #checkDomElements(messageId) { // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure) if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) { this.messageDom = null; } // Cache the DOM elements once if (this.messageDom === null) { this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`); if (this.messageDom === null) throw new Error('message dom does not exist'); } if (this.messageReasoningDetailsDom === null) { this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details'); } if (this.messageReasoningContentDom === null) { this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning'); } if (this.messageReasoningHeaderDom === null) { this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title'); } } /** * Updates the reasoning time display in the UI. * * Shows the duration in a human-readable format with a tooltip for exact seconds. * Displays "Thinking..." if still processing, or a generic message otherwise. */ #updateReasoningTimeUI() { const element = this.messageReasoningHeaderDom; const duration = this.getDuration(); let data = null; let title = ''; if (duration) { const seconds = moment.duration(duration).asSeconds(); const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 }); element.textContent = t`Thought for ${durationStr}`; data = String(seconds); title = `${seconds} seconds`; } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) { element.textContent = t`Thought for some time`; data = 'unknown'; } else { element.textContent = t`Thinking...`; data = null; } if (this.type !== ReasoningType.Model) { title += ` [${translate(this.type)}]`; title = title.trim(); } element.title = title; setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data); setDatasetProperty(element, 'duration', data); } } /** * Helper class for adding reasoning to messages. * Keeps track of the number of reasoning additions. */ export class PromptReasoning { static REASONING_PLACEHOLDER = '\u200B'; constructor() { this.counter = 0; } /** * Checks if the limit of reasoning additions has been reached. * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise. */ isLimitReached() { if (!power_user.reasoning.add_to_prompts) { return true; } return this.counter >= power_user.reasoning.max_additions; } /** * Add reasoning to a message according to the power user settings. * @param {string} content Message content * @param {string} reasoning Message reasoning * @param {boolean} isPrefix Whether this is the last message prefix * @returns {string} Message content with reasoning */ addToMessage(content, reasoning, isPrefix) { // Disabled or reached limit of additions if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) { return content; } // No reasoning provided or a legacy placeholder if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { return content; } // Increment the counter this.counter++; // Substitute macros in variable parts const prefix = substituteParams(power_user.reasoning.prefix || ''); const separator = substituteParams(power_user.reasoning.separator || ''); const suffix = substituteParams(power_user.reasoning.suffix || ''); // Combine parts with reasoning only if (isPrefix && !content) { return `${prefix}${reasoning}`; } // Combine parts with reasoning and content return `${prefix}${reasoning}${suffix}${separator}${content}`; } } function loadReasoningSettings() { $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts); $('#reasoning_add_to_prompts').on('change', function () { power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); saveSettingsDebounced(); }); $('#reasoning_prefix').val(power_user.reasoning.prefix); $('#reasoning_prefix').on('input', function () { power_user.reasoning.prefix = String($(this).val()); saveSettingsDebounced(); }); $('#reasoning_suffix').val(power_user.reasoning.suffix); $('#reasoning_suffix').on('input', function () { power_user.reasoning.suffix = String($(this).val()); saveSettingsDebounced(); }); $('#reasoning_separator').val(power_user.reasoning.separator); $('#reasoning_separator').on('input', function () { power_user.reasoning.separator = String($(this).val()); saveSettingsDebounced(); }); $('#reasoning_max_additions').val(power_user.reasoning.max_additions); $('#reasoning_max_additions').on('input', function () { power_user.reasoning.max_additions = Number($(this).val()); saveSettingsDebounced(); }); $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse); $('#reasoning_auto_parse').on('change', function () { power_user.reasoning.auto_parse = !!$(this).prop('checked'); saveSettingsDebounced(); }); $('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand); $('#reasoning_auto_expand').on('change', function () { power_user.reasoning.auto_expand = !!$(this).prop('checked'); toggleReasoningAutoExpand(); saveSettingsDebounced(); }); toggleReasoningAutoExpand(); $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden); $('#reasoning_show_hidden').on('change', function () { power_user.reasoning.show_hidden = !!$(this).prop('checked'); $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null); saveSettingsDebounced(); }); $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null); } function registerReasoningSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'reasoning-get', aliases: ['get-reasoning'], returns: ARGUMENT_TYPE.STRING, 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.`, unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'Message ID. If not provided, the message ID of the last message is used.', typeList: ARGUMENT_TYPE.NUMBER, enumProvider: commonEnumProviders.messages(), }), ], callback: (_args, value) => { const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1; const message = chat[messageId]; const reasoning = String(message?.extra?.reasoning ?? ''); return reasoning; }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'reasoning-set', aliases: ['set-reasoning'], returns: ARGUMENT_TYPE.STRING, helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`, namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'at', description: 'Message ID. If not provided, the message ID of the last message is used.', typeList: ARGUMENT_TYPE.NUMBER, enumProvider: commonEnumProviders.messages(), }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'Reasoning block content.', typeList: ARGUMENT_TYPE.STRING, }), ], callback: async (args, value) => { const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1; const message = chat[messageId]; if (!message) { return ''; } // Make sure the message has an extra object if (!message.extra || typeof message.extra !== 'object') { message.extra = {}; } message.extra.reasoning = String(value ?? ''); message.extra.reasoning_type = ReasoningType.Manual; await saveChatConditional(); closeMessageEditor('reasoning'); updateMessageBlock(messageId, message); return message.extra.reasoning; }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'reasoning-parse', aliases: ['parse-reasoning'], returns: 'reasoning string', helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`, namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'regex', description: 'Whether to apply regex scripts to the reasoning content.', typeList: [ARGUMENT_TYPE.BOOLEAN], defaultValue: 'true', isRequired: false, enumList: commonEnumProviders.boolean('trueFalse')(), }), SlashCommandNamedArgument.fromProps({ name: 'return', description: 'Whether to return the parsed reasoning or the content without reasoning', typeList: [ARGUMENT_TYPE.STRING], defaultValue: 'reasoning', isRequired: false, enumList: [ new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning), new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message), ], }), SlashCommandNamedArgument.fromProps({ name: 'strict', description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).', typeList: [ARGUMENT_TYPE.BOOLEAN], defaultValue: 'true', isRequired: false, enumList: commonEnumProviders.boolean('trueFalse')(), }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'input string', typeList: [ARGUMENT_TYPE.STRING], }), ], callback: (args, value) => { if (!value || typeof value !== 'string') { return ''; } if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) { toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`); return value; } if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) { toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`); } const returnMessage = args.return === 'content'; const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) }); if (!parsedReasoning) { return returnMessage ? value : ''; } if (returnMessage) { return parsedReasoning.content; } const applyRegex = !isFalseBoolean(String(args.regex ?? '')); return applyRegex ? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING) : parsedReasoning.reasoning; }, })); } function registerReasoningMacros() { MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`); MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`); MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`); } function setReasoningEventHandlers() { $(document).on('click', '.mes_reasoning_details', function (e) { if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) { e.preventDefault(); } }); $(document).on('click', '.mes_reasoning_header', function (e) { const details = $(this).closest('.mes_reasoning_details'); // Along with the CSS rules to mark blocks not toggle-able when they are empty, prevent them from actually being toggled, or being edited if (details.find('.mes_reasoning').is(':empty')) { e.preventDefault(); return; } // If we are in message edit mode and reasoning area is closed, a click opens and edits it const mes = $(this).closest('.mes'); const mesEditArea = mes.find('#curEditTextarea'); if (mesEditArea.length) { const summary = $(mes).find('.mes_reasoning_summary'); if (!summary.attr('open')) { summary.find('.mes_reasoning_edit').trigger('click'); } } }); $(document).on('click', '.mes_reasoning_copy', (e) => { e.stopPropagation(); e.preventDefault(); }); $(document).on('click', '.mes_reasoning_edit', function (e) { e.stopPropagation(); e.preventDefault(); const { message, messageBlock } = getMessageFromJquery(this); if (!message?.extra) { return; } const reasoning = String(message?.extra?.reasoning ?? ''); const chatElement = document.getElementById('chat'); const textarea = document.createElement('textarea'); const reasoningBlock = messageBlock.find('.mes_reasoning'); textarea.classList.add('reasoning_edit_textarea'); textarea.value = reasoning; $(textarea).insertBefore(reasoningBlock); if (!CSS.supports('field-sizing', 'content')) { const resetHeight = function () { const scrollTop = chatElement.scrollTop; textarea.style.height = '0px'; textarea.style.height = `${textarea.scrollHeight}px`; chatElement.scrollTop = scrollTop; }; textarea.addEventListener('input', resetHeight); resetHeight(); } textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); const textareaRect = textarea.getBoundingClientRect(); const chatRect = chatElement.getBoundingClientRect(); // Scroll if textarea bottom is below visible area if (textareaRect.bottom > chatRect.bottom) { const scrollOffset = textareaRect.bottom - chatRect.bottom; chatElement.scrollTop += scrollOffset; } }); $(document).on('click', '.mes_reasoning_edit_done', async function (e) { e.stopPropagation(); e.preventDefault(); const { message, messageId, messageBlock } = getMessageFromJquery(this); if (!message?.extra) { return; } const textarea = messageBlock.find('.reasoning_edit_textarea'); const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true }); message.extra.reasoning = reasoning; message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual; await saveChatConditional(); updateMessageBlock(messageId, message); textarea.remove(); messageBlock.find('.mes_edit_done:visible').trigger('click'); }); $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { e.stopPropagation(); e.preventDefault(); const { messageBlock } = getMessageFromJquery(this); const textarea = messageBlock.find('.reasoning_edit_textarea'); textarea.remove(); messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click'); updateReasoningUI(messageBlock); }); $(document).on('click', '.mes_edit_add_reasoning', async function () { const { message, messageBlock } = getMessageFromJquery(this); if (!message?.extra) { return; } if (message.extra.reasoning) { toastr.info(t`Reasoning already exists.`, t`Edit Message`); return; } messageBlock.addClass('reasoning'); // To make hidden reasoning blocks editable, we just set them to "Done" here already. // They will be done on save anyway - and on cancel the reasoning block gets rerendered too. if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) { messageBlock.attr('data-reasoning-state', ReasoningState.Done); } // Open the reasoning area so we can actually edit it messageBlock.find('.mes_reasoning_details').attr('open', ''); messageBlock.find('.mes_reasoning_edit').trigger('click'); await saveChatConditional(); }); $(document).on('click', '.mes_reasoning_delete', async function (e) { e.stopPropagation(); e.preventDefault(); const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?
Visible message contents will stay intact.`); if (!confirm) { return; } const { message, messageId, messageBlock } = getMessageFromJquery(this); if (!message?.extra) { return; } message.extra.reasoning = ''; delete message.extra.reasoning_type; delete message.extra.reasoning_duration; await saveChatConditional(); updateMessageBlock(messageId, message); const textarea = messageBlock.find('.reasoning_edit_textarea'); textarea.remove(); }); $(document).on('pointerup', '.mes_reasoning_copy', async function () { const { message } = getMessageFromJquery(this); const reasoning = String(message?.extra?.reasoning ?? ''); if (!reasoning) { return; } await copyText(reasoning); toastr.info(t`Copied!`, '', { timeOut: 2000 }); }); } /** * Removes reasoning from a string if auto-parsing is enabled. * @param {string} str Input string * @returns {string} Output string */ export function removeReasoningFromString(str) { if (!power_user.reasoning.auto_parse) { return str; } const parsedReasoning = parseReasoningFromString(str); return parsedReasoning?.content ?? str; } /** * Parses reasoning from a string using the power user reasoning settings. * @typedef {Object} ParsedReasoning * @property {string} reasoning Reasoning block * @property {string} content Message content * @param {string} str Content of the message * @param {Object} options Optional arguments * @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it * @returns {ParsedReasoning|null} Parsed reasoning block and message content */ function parseReasoningFromString(str, { strict = true } = {}) { // Both prefix and suffix must be defined if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) { return null; } try { const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's'); let didReplace = false; let reasoning = ''; let content = String(str).replace(regex, (_match, captureGroup) => { didReplace = true; reasoning = captureGroup; return ''; }); if (didReplace) { reasoning = trimSpaces(reasoning); content = trimSpaces(content); } return { reasoning, content }; } catch (error) { console.error('[Reasoning] Error parsing reasoning block', error); return null; } } function registerReasoningAppEvents() { eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => { if (!power_user.reasoning.auto_parse) { return; } console.debug('[Reasoning] Auto-parsing reasoning block for message', idx); const message = chat[idx]; if (!message) { console.warn('[Reasoning] Message not found', idx); return null; } if (!message.mes || message.mes === '...') { console.debug('[Reasoning] Message content is empty or a placeholder', idx); return null; } if (message.extra?.reasoning) { console.debug('[Reasoning] Message already has reasoning', idx); return null; } const parsedReasoning = parseReasoningFromString(message.mes); // No reasoning block found if (!parsedReasoning) { return; } // Make sure the message has an extra object if (!message.extra || typeof message.extra !== 'object') { message.extra = {}; } const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes; // If reasoning was found, add it to the message if (parsedReasoning.reasoning) { message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING); message.extra.reasoning_type = ReasoningType.Parsed; } // Update the message text if it was changed if (parsedReasoning.content !== message.mes) { message.mes = parsedReasoning.content; } // Find if a message already exists in DOM and must be updated if (contentUpdated) { const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null; if (messageRendered) { console.debug('[Reasoning] Updating message block', idx); updateMessageBlock(idx, message); } } }); } export function initReasoning() { loadReasoningSettings(); setReasoningEventHandlers(); registerReasoningSlashCommands(); registerReasoningMacros(); registerReasoningAppEvents(); }