Add reasoning auto-parsing, always show reasoning prefix/suffix in Markdown, respect space trim preference

d0abba23dca061bb1875a2bb62319810a1f70976

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

4 files changed, +117 -3Showing whitespace changes
public/index.html+6 -0
@@ -3791,6 +3791,12 @@
37913791 <span data-i18n="Reasoning">Reasoning</span>
37923792 </h4>
37933793 <div>
3794+ <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">
3795+ <input id="reasoning_auto_parse" type="checkbox" />
3796+ <small data-i18n="Auto-Parse Reasoning">
3797+ Auto-Parse Reasoning
3798+ </small>
3799+ </label>
37943800 <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">
37953801 <input id="reasoning_add_to_prompts" type="checkbox" />
37963802 <small data-i18n="Add Reasoning to Prompts">
public/script.js+17 -1
@@ -169,6 +169,7 @@ import {
169169 toggleDrawer,
170170 isElementInViewport,
171171 copyText,
172+ escapeHtml,
172173} from './scripts/utils.js';
173174import { debounce_timeout } from './scripts/constants.js';
174175
@@ -2066,6 +2067,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20662067 mes = mes.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
20672068 }
20682069
2070+ // Make sure reasoning strings are always shown, even if they include "<" or ">"
2071+ [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
2072+ if (!reasoningString || !reasoningString.trim().length) {
2073+ return;
2074+ }
2075+ // Only replace the first occurrence of the reasoning string
2076+ if (mes.includes(reasoningString)) {
2077+ mes = mes.replace(reasoningString, escapeHtml(reasoningString));
2078+ }
2079+ });
2080+
20692081 if (!isSystem) {
20702082 // Save double quotes in tags as a special character to prevent them from being encoded
20712083 if (!power_user.encode_tags) {
@@ -3208,7 +3220,7 @@ class StreamingProcessor {
32083220 }
32093221
32103222 if (this.reasoning) {
32113223 chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
32123224 if (this.messageReasoningDom instanceof HTMLElement) {
32133225 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
32143226 this.messageReasoningDom.innerHTML = formattedReasoning;
@@ -4805,6 +4817,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
48054817 messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
48064818 reasoning = getRegexedString(reasoning, regex_placement.REASONING);
48074819
4820+ if (power_user.trim_spaces) {
4821+ reasoning = reasoning.trim();
4822+ }
4823+
48084824 if (isContinue) {
48094825 getMessage = continue_mag + getMessage;
48104826 }
public/scripts/power-user.js+1 -0
@@ -254,6 +254,7 @@ let power_user = {
254254 },
255255
256256 reasoning: {
257+ auto_parse: false,
257258 add_to_prompts: false,
258259 prefix: '<think>\n',
259260 suffix: '\n</think>',
public/scripts/reasoning.js+93 -2
@@ -1,4 +1,4 @@
11import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
22import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
33import { t } from './i18n.js';
44import { MacrosParser } from './macros.js';
@@ -8,7 +8,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
88import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
99import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
1010import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1111import { copyText, escapeRegex } from './utils.js';
1212
1313/**
1414 * Gets a message from a jQuery element.
@@ -106,6 +106,12 @@ function loadReasoningSettings() {
106106 power_user.reasoning.max_additions = Number($(this).val());
107107 saveSettingsDebounced();
108108 });
109+
110+ $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
111+ $('#reasoning_auto_parse').on('change', function () {
112+ power_user.reasoning.auto_parse = !!$(this).prop('checked');
113+ saveSettingsDebounced();
114+ });
109115}
110116
111117function registerReasoningSlashCommands() {
@@ -290,9 +296,94 @@ function setReasoningEventHandlers(){
290296 });
291297}
292298
299+/**
300+ * Parses reasoning from a string using the power user reasoning settings.
301+ * @typedef {Object} ParsedReasoning
302+ * @property {string} reasoning Reasoning block
303+ * @property {string} content Message content
304+ * @param {string} str Content of the message
305+ * @returns {ParsedReasoning|null} Parsed reasoning block and message content
306+ */
307+function parseReasoningFromString(str) {
308+ // Both prefix and suffix must be defined
309+ if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
310+ return null;
311+ }
312+
313+ try {
314+ let reasoning = '';
315+
316+ const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
317+ const content = String(str).replace(regex, (_match, captureGroup) => {
318+ reasoning = power_user.trim_spaces ? captureGroup.trim() : captureGroup;
319+ return '';
320+ });
321+
322+ return { reasoning, content };
323+ } catch (error) {
324+ console.error('[Reasoning] Error parsing reasoning block', error);
325+ return null;
326+ }
327+}
328+
329+function registerReasoningAppEvents() {
330+ eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
331+ if (!power_user.reasoning.auto_parse) {
332+ return;
333+ }
334+
335+ console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
336+ const message = chat[idx];
337+
338+ if (!message) {
339+ console.warn('[Reasoning] Message not found', idx);
340+ return null;
341+ }
342+
343+ if (!message.mes || message.mes === '...') {
344+ console.debug('[Reasoning] Message content is empty or a placeholder', idx);
345+ return null;
346+ }
347+
348+ const parsedReasoning = parseReasoningFromString(message.mes);
349+
350+ // No reasoning block found
351+ if (!parsedReasoning) {
352+ return;
353+ }
354+
355+ // Make sure the message has an extra object
356+ if (!message.extra || typeof message.extra !== 'object') {
357+ message.extra = {};
358+ }
359+
360+ const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
361+
362+ // If reasoning was found, add it to the message
363+ if (parsedReasoning.reasoning) {
364+ message.extra.reasoning = parsedReasoning.reasoning;
365+ }
366+
367+ // Update the message text if it was changed
368+ if (parsedReasoning.content !== message.mes) {
369+ message.mes = parsedReasoning.content;
370+ }
371+
372+ // Find if a message already exists in DOM and must be updated
373+ if (contentUpdated) {
374+ const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
375+ if (messageRendered) {
376+ console.debug('[Reasoning] Updating message block', idx);
377+ updateMessageBlock(idx, message);
378+ }
379+ }
380+ });
381+}
382+
293383export function initReasoning() {
294384 loadReasoningSettings();
295385 setReasoningEventHandlers();
296386 registerReasoningSlashCommands();
297387 registerReasoningMacros();
388+ registerReasoningAppEvents();
298389}