Merge pull request #3444 from SillyTavern/hidden-reasoning-tracking Track reasoning duration for models with hidden reasoning

25a2582d15bf63e86ae590e156a21bd6331eeeaf

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

Signed
8 files changed, +441 -145Showing whitespace changes
public/index.html+8 -2
@@ -1983,12 +1983,12 @@
1983 </span>1983 </span>
1984 </div>1984 </div>
1985 </div>1985 </div>
1986 <div class="range-block" data-source="makersuite,deepseek,openrouter">1986 <div class="range-block" data-source="deepseek,openrouter">
1987 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">1987 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
1988 <input id="openai_show_thoughts" type="checkbox" />1988 <input id="openai_show_thoughts" type="checkbox" />
1989 <span>1989 <span>
1990 <span data-i18n="Request model reasoning">Request model reasoning</span>1990 <span data-i18n="Request model reasoning">Request model reasoning</span>
1991 <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>1991 <i class="opacity50p fa-solid fa-circle-info" title="DeepSeek Reasoner"></i>
1992 </span>1992 </span>
1993 </label>1993 </label>
1994 <div class="toggle-description justifyLeft marginBot5">1994 <div class="toggle-description justifyLeft marginBot5">
@@ -3810,6 +3810,12 @@
3810 Auto-Expand3810 Auto-Expand
3811 </small>3811 </small>
3812 </label>3812 </label>
3813 <label class="checkbox_label flex1" for="reasoning_show_hidden" title="Show reasoning time for models with hidden reasoning." data-i18n="[title]reasoning_show_hidden">
3814 <input id="reasoning_show_hidden" type="checkbox" />
3815 <small data-i18n="Show Hidden">
3816 Show Hidden
3817 </small>
3818 </label>
3813 </div>3819 </div>
3814 <div class="flex-container alignItemsBaseline">3820 <div class="flex-container alignItemsBaseline">
3815 <label class="checkbox_label flex1" 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">3821 <label class="checkbox_label flex1" 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">
public/script.js+43 -108
@@ -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 { extractReasoningFromData, initReasoning, PromptReasoning, removeReasoningFromString, updateReasoningTimeUI } from './scripts/reasoning.js';272import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
273import { accountStorage } from './scripts/util/AccountStorage.js';273import { accountStorage } from './scripts/util/AccountStorage.js';
274274
275// API OBJECT FOR EXTERNAL WIRING275// API OBJECT FOR EXTERNAL WIRING
@@ -2224,8 +2224,6 @@ function getMessageFromTemplate({
2224 isUser,2224 isUser,
2225 avatarImg,2225 avatarImg,
2226 bias,2226 bias,
2227 reasoning,
2228 reasoningDuration,
2229 isSystem,2227 isSystem,
2230 title,2228 title,
2231 timerValue,2229 timerValue,
@@ -2250,7 +2248,6 @@ function getMessageFromTemplate({
2250 mes.find('.avatar img').attr('src', avatarImg);2248 mes.find('.avatar img').attr('src', avatarImg);
2251 mes.find('.ch_name .name_text').text(characterName);2249 mes.find('.ch_name .name_text').text(characterName);
2252 mes.find('.mes_bias').html(bias);2250 mes.find('.mes_bias').html(bias);
2253 mes.find('.mes_reasoning').html(reasoning);
2254 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);2251 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
2255 mes.find('.mesIDDisplay').text(`#${mesId}`);2252 mes.find('.mesIDDisplay').text(`#${mesId}`);
2256 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);2253 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
@@ -2258,12 +2255,7 @@ function getMessageFromTemplate({
2258 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);2255 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
2259 bookmarkLink && updateBookmarkDisplay(mes);2256 bookmarkLink && updateBookmarkDisplay(mes);
22602257
2261 if (reasoning) {2258 updateReasoningUI(mes);
2262 mes.addClass('reasoning');
2263 }
2264 if (reasoningDuration) {
2265 updateReasoningTimeUI(mes.find('.mes_reasoning_header_title')[0], reasoningDuration, { forceEnd: true });
2266 }
22672259
2268 if (power_user.timestamp_model_icon && extra?.api) {2260 if (power_user.timestamp_model_icon && extra?.api) {
2269 insertSVGIcon(mes, extra);2261 insertSVGIcon(mes, extra);
@@ -2285,8 +2277,9 @@ export function updateMessageBlock(messageId, message, { rerenderMessage = true
2285 const text = message?.extra?.display_text ?? message.mes;2277 const text = message?.extra?.display_text ?? message.mes;
2286 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));2278 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2287 }2279 }
2288 messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));2280
2289 messageElement.toggleClass('reasoning', !!message.extra?.reasoning);2281 updateReasoningUI(messageElement);
2282
2290 addCopyToCodeBlocks(messageElement);2283 addCopyToCodeBlocks(messageElement);
2291 appendMediaToMessage(message, messageElement);2284 appendMediaToMessage(message, messageElement);
2292}2285}
@@ -2446,7 +2439,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2446 false,2439 false,
2447 );2440 );
2448 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);2441 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2449 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
2450 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2442 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24512443
2452 let params = {2444 let params = {
@@ -2456,8 +2448,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2456 isUser: mes.is_user,2448 isUser: mes.is_user,
2457 avatarImg: avatarImg,2449 avatarImg: avatarImg,
2458 bias: bias,2450 bias: bias,
2459 reasoning: reasoning,
2460 reasoningDuration: mes.extra?.reasoning_duration,
2461 isSystem: isSystem,2451 isSystem: isSystem,
2462 title: title,2452 title: title,
2463 bookmarkLink: bookmarkLink,2453 bookmarkLink: bookmarkLink,
@@ -2465,7 +2455,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2465 timestamp: timestamp,2455 timestamp: timestamp,
2466 extra: mes.extra,2456 extra: mes.extra,
2467 tokenCount: mes.extra?.token_count ?? 0,2457 tokenCount: mes.extra?.token_count ?? 0,
2468 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count),2458 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration),
2469 };2459 };
24702460
2471 const renderedMessage = getMessageFromTemplate(params);2461 const renderedMessage = getMessageFromTemplate(params);
@@ -2517,8 +2507,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2517 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);2507 const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
2518 swipeMessage.attr('swipeid', params.swipeId);2508 swipeMessage.attr('swipeid', params.swipeId);
2519 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2509 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2520 swipeMessage.find('.mes_reasoning').html(reasoning);
2521 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);2510 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2511 updateReasoningUI(swipeMessage);
2522 appendMediaToMessage(mes, swipeMessage);2512 appendMediaToMessage(mes, swipeMessage);
2523 if (power_user.timestamp_model_icon && params.extra?.api) {2513 if (power_user.timestamp_model_icon && params.extra?.api) {
2524 insertSVGIcon(swipeMessage, params.extra);2514 insertSVGIcon(swipeMessage, params.extra);
@@ -2585,13 +2575,14 @@ export function formatCharacterAvatar(characterAvatar) {
2585 * @param {Date} gen_started Date when generation was started2575 * @param {Date} gen_started Date when generation was started
2586 * @param {Date} gen_finished Date when generation was finished2576 * @param {Date} gen_finished Date when generation was finished
2587 * @param {number} tokenCount Number of tokens generated (0 if not available)2577 * @param {number} tokenCount Number of tokens generated (0 if not available)
2578 * @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done)
2588 * @returns {Object} Object containing the formatted timer value and title2579 * @returns {Object} Object containing the formatted timer value and title
2589 * @example2580 * @example
2590 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);2581 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
2591 * console.log(timerValue); // 1.2s2582 * console.log(timerValue); // 1.2s
2592 * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s2583 * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s
2593 */2584 */
2594function formatGenerationTimer(gen_started, gen_finished, tokenCount) {2585function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) {
2595 if (!gen_started || !gen_finished) {2586 if (!gen_started || !gen_finished) {
2596 return {};2587 return {};
2597 }2588 }
@@ -2605,8 +2596,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
2605 `Generation queued: ${start.format(dateFormat)}`,2596 `Generation queued: ${start.format(dateFormat)}`,
2606 `Reply received: ${finish.format(dateFormat)}`,2597 `Reply received: ${finish.format(dateFormat)}`,
2607 `Time to generate: ${seconds} seconds`,2598 `Time to generate: ${seconds} seconds`,
2599 reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '',
2608 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',2600 tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
2609 ].join('\n');2601 ].filter(x => x).join('\n').trim();
26102602
2611 if (isNaN(seconds) || seconds < 0) {2603 if (isNaN(seconds) || seconds < 0) {
2612 return { timerValue: '', timerTitle };2604 return { timerValue: '', timerTitle };
@@ -3125,15 +3117,14 @@ class StreamingProcessor {
3125 constructor(type, forceName2, timeStarted, continueMessage) {3117 constructor(type, forceName2, timeStarted, continueMessage) {
3126 this.result = '';3118 this.result = '';
3127 this.messageId = -1;3119 this.messageId = -1;
3120 /** @type {HTMLElement} */
3128 this.messageDom = null;3121 this.messageDom = null;
3122 /** @type {HTMLElement} */
3129 this.messageTextDom = null;3123 this.messageTextDom = null;
3124 /** @type {HTMLElement} */
3130 this.messageTimerDom = null;3125 this.messageTimerDom = null;
3131 /** @type {HTMLElement} */3126 /** @type {HTMLElement} */
3132 this.messageTokenCounterDom = null;3127 this.messageTokenCounterDom = null;
3133 /** @type {HTMLElement} */
3134 this.messageReasoningDom = null;
3135 /** @type {HTMLElement} */
3136 this.messageReasoningHeaderDom = null;
3137 /** @type {HTMLTextAreaElement} */3128 /** @type {HTMLTextAreaElement} */
3138 this.sendTextarea = document.querySelector('#send_textarea');3129 this.sendTextarea = document.querySelector('#send_textarea');
3139 this.type = type;3130 this.type = type;
@@ -3149,16 +3140,8 @@ class StreamingProcessor {
3149 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */3140 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
3150 this.messageLogprobs = [];3141 this.messageLogprobs = [];
3151 this.toolCalls = [];3142 this.toolCalls = [];
3152 this.reasoning = '';3143 // Initialize reasoning in its own handler
3153 this.reasoningStartTime = null;3144 this.reasoningHandler = new ReasoningHandler(timeStarted);
3154 this.reasoningEndTime = null;
3155 }
3156
3157 #reasoningDuration() {
3158 if (this.reasoningStartTime && this.reasoningEndTime) {
3159 return (this.reasoningEndTime - this.reasoningStartTime);
3160 }
3161 return null;
3162 }3145 }
31633146
3164 #checkDomElements(messageId) {3147 #checkDomElements(messageId) {
@@ -3167,9 +3150,8 @@ class StreamingProcessor {
3167 this.messageTextDom = this.messageDom?.querySelector('.mes_text');3150 this.messageTextDom = this.messageDom?.querySelector('.mes_text');
3168 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');3151 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
3169 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');3152 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3170 this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
3171 this.messageReasoningHeaderDom = this.messageDom?.querySelector('.mes_reasoning_header_title');
3172 }3153 }
3154 this.reasoningHandler.updateDom(messageId);
3173 }3155 }
31743156
3175 #updateMessageBlockVisibility() {3157 #updateMessageBlockVisibility() {
@@ -3179,22 +3161,12 @@ class StreamingProcessor {
3179 }3161 }
3180 }3162 }
31813163
3182 showMessageButtons(messageId) {3164 markUIGenStarted() {
3183 if (messageId == -1) {3165 deactivateSendButtons();
3184 return;
3185 }
3186
3187 showStopButton();
3188 $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
3189 }
3190
3191 hideMessageButtons(messageId) {
3192 if (messageId == -1) {
3193 return;
3194 }3166 }
31953167
3196 hideStopButton();3168 markUIGenStopped() {
3197 $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });3169 activateSendButtons();
3198 }3170 }
31993171
3200 async onStartStreaming(text) {3172 async onStartStreaming(text) {
@@ -3203,14 +3175,12 @@ class StreamingProcessor {
3203 if (this.type == 'impersonate') {3175 if (this.type == 'impersonate') {
3204 this.sendTextarea.value = '';3176 this.sendTextarea.value = '';
3205 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3177 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3206 }3178 } else {
3207 else {
3208 await saveReply(this.type, text, true, '', [], '');3179 await saveReply(this.type, text, true, '', [], '');
3209 messageId = chat.length - 1;3180 messageId = chat.length - 1;
3210 this.#checkDomElements(messageId);3181 this.#checkDomElements(messageId);
3211 this.showMessageButtons(messageId);3182 this.markUIGenStarted();
3212 }3183 }
3213
3214 hideSwipeButtons();3184 hideSwipeButtons();
3215 scrollChatToBottom();3185 scrollChatToBottom();
3216 return messageId;3186 return messageId;
@@ -3228,11 +3198,9 @@ class StreamingProcessor {
32283198
3229 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);3199 let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
32303200
3231 // Predict unbalanced asterisks / quotes during streaming
3232 const charsToBalance = ['*', '"', '```'];3201 const charsToBalance = ['*', '"', '```'];
3233 for (const char of charsToBalance) {3202 for (const char of charsToBalance) {
3234 if (!isFinal && isOdd(countOccurrences(processedText, char))) {3203 if (!isFinal && isOdd(countOccurrences(processedText, char))) {
3235 // Add character at the end to balance it
3236 const separator = char.length > 1 ? '\n' : '';3204 const separator = char.length > 1 ? '\n' : '';
3237 processedText = processedText.trimEnd() + separator + char;3205 processedText = processedText.trimEnd() + separator + char;
3238 }3206 }
@@ -3241,47 +3209,24 @@ class StreamingProcessor {
3241 if (isImpersonate) {3209 if (isImpersonate) {
3242 this.sendTextarea.value = processedText;3210 this.sendTextarea.value = processedText;
3243 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3211 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3244 }3212 } else {
3245 else {
3246 const mesChanged = chat[messageId]['mes'] !== processedText;3213 const mesChanged = chat[messageId]['mes'] !== processedText;
3247
3248 this.#checkDomElements(messageId);3214 this.#checkDomElements(messageId);
3249 this.#updateMessageBlockVisibility();3215 this.#updateMessageBlockVisibility();
3250 const currentTime = new Date();3216 const currentTime = new Date();
3251 chat[messageId]['mes'] = processedText;3217 chat[messageId]['mes'] = processedText;
3252 chat[messageId]['gen_started'] = this.timeStarted;3218 chat[messageId]['gen_started'] = this.timeStarted;
3253 chat[messageId]['gen_finished'] = currentTime;3219 chat[messageId]['gen_finished'] = currentTime;
3254
3255 if (!chat[messageId]['extra']) {3220 if (!chat[messageId]['extra']) {
3256 chat[messageId]['extra'] = {};3221 chat[messageId]['extra'] = {};
3257 }3222 }
32583223
3259 if (this.reasoning) {3224 // Update reasoning
3260 const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;3225 await this.reasoningHandler.process(messageId, mesChanged);
3261 const reasoningChanged = chat[messageId]['extra']['reasoning'] !== reasoning;
3262 chat[messageId]['extra']['reasoning'] = reasoning;
3263
3264 if (reasoningChanged && this.reasoningStartTime === null) {
3265 this.reasoningStartTime = Date.now();
3266 }
3267 if (!reasoningChanged && mesChanged && this.reasoningStartTime !== null && this.reasoningEndTime === null) {
3268 this.reasoningEndTime = Date.now();
3269 }
3270 await this.#updateReasoningTime(messageId);
32713226
3272 if (this.messageReasoningDom instanceof HTMLElement) {3227 // Token count update.
3273 const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);3228 const tokenCountText = this.reasoningHandler.reasoning + processedText;
3274 this.messageReasoningDom.innerHTML = formattedReasoning;
3275 }
3276 if (this.messageDom instanceof HTMLElement) {
3277 this.messageDom.classList.add('reasoning');
3278 }
3279 }
3280
3281 // Don't waste time calculating token count for streaming
3282 const tokenCountText = (this.reasoning || '') + processedText;
3283 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;3229 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
3284
3285 if (currentTokenCount) {3230 if (currentTokenCount) {
3286 chat[messageId]['extra']['token_count'] = currentTokenCount;3231 chat[messageId]['extra']['token_count'] = currentTokenCount;
3287 if (this.messageTokenCounterDom instanceof HTMLElement) {3232 if (this.messageTokenCounterDom instanceof HTMLElement) {
@@ -3307,7 +3252,7 @@ class StreamingProcessor {
3307 this.messageTextDom.innerHTML = formattedText;3252 this.messageTextDom.innerHTML = formattedText;
3308 }3253 }
33093254
3310 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);3255 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration());
3311 if (this.messageTimerDom instanceof HTMLElement) {3256 if (this.messageTimerDom instanceof HTMLElement) {
3312 this.messageTimerDom.textContent = timePassed.timerValue;3257 this.messageTimerDom.textContent = timePassed.timerValue;
3313 this.messageTimerDom.title = timePassed.timerTitle;3258 this.messageTimerDom.title = timePassed.timerTitle;
@@ -3321,23 +3266,12 @@ class StreamingProcessor {
3321 }3266 }
3322 }3267 }
33233268
3324 async #updateReasoningTime(messageId, { forceEnd = false } = {}) {
3325 const duration = this.#reasoningDuration();
3326 chat[messageId]['extra']['reasoning_duration'] = duration;
3327 updateReasoningTimeUI(this.messageReasoningHeaderDom, duration, { forceEnd: forceEnd });
3328 await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, duration);
3329 }
3330
3331 async onFinishStreaming(messageId, text) {3269 async onFinishStreaming(messageId, text) {
3332 this.hideMessageButtons(this.messageId);3270 this.markUIGenStopped();
3333 await this.onProgressStreaming(messageId, text, true);3271 await this.onProgressStreaming(messageId, text, true);
3334 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));3272 addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
33353273
3336 // Ensure reasoning finish time is recorded if not already3274 await this.reasoningHandler.finish(messageId);
3337 if (this.reasoningStartTime !== null && this.reasoningEndTime === null) {
3338 this.reasoningEndTime = Date.now();
3339 await this.#updateReasoningTime(messageId, { forceEnd: true });
3340 }
33413275
3342 if (Array.isArray(this.swipes) && this.swipes.length > 0) {3276 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
3343 const message = chat[messageId];3277 const message = chat[messageId];
@@ -3378,7 +3312,7 @@ class StreamingProcessor {
3378 this.abortController.abort();3312 this.abortController.abort();
3379 this.isStopped = true;3313 this.isStopped = true;
33803314
3381 this.hideMessageButtons(this.messageId);3315 this.markUIGenStopped();
3382 generatedPromptCache = '';3316 generatedPromptCache = '';
3383 unblockGeneration();3317 unblockGeneration();
33843318
@@ -3438,7 +3372,8 @@ class StreamingProcessor {
3438 if (logprobs) {3372 if (logprobs) {
3439 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3373 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3440 }3374 }
3441 this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);3375 // Get the updated reasoning string into the handler
3376 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');
3442 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3377 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3443 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));3378 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
3444 }3379 }
@@ -6195,20 +6130,21 @@ function extractImageFromMessage(getMessage) {
6195 return { getMessage, image, title };6130 return { getMessage, image, title };
6196}6131}
61976132
6133/**
6134 * A function mainly used to switch 'generating' state - setting it to false and activating the buttons again
6135 */
6198export function activateSendButtons() {6136export function activateSendButtons() {
6199 is_send_press = false;6137 is_send_press = false;
6200 $('#send_but').removeClass('displayNone');
6201 $('#mes_continue').removeClass('displayNone');
6202 $('#mes_impersonate').removeClass('displayNone');
6203 $('.mes_buttons:last').show();
6204 hideStopButton();6138 hideStopButton();
6139 delete document.body.dataset.generating;
6205}6140}
62066141
6142/**
6143 * A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons
6144 */
6207export function deactivateSendButtons() {6145export function deactivateSendButtons() {
6208 $('#send_but').addClass('displayNone');
6209 $('#mes_continue').addClass('displayNone');
6210 $('#mes_impersonate').addClass('displayNone');
6211 showStopButton();6146 showStopButton();
6147 document.body.dataset.generating = 'true';
6212}6148}
62136149
6214export function resetChatState() {6150export function resetChatState() {
@@ -8801,7 +8737,7 @@ const swipe_right = () => {
8801 // resets the timer8737 // resets the timer
8802 swipeMessage.find('.mes_timer').html('');8738 swipeMessage.find('.mes_timer').html('');
8803 swipeMessage.find('.tokenCounterDisplay').text('');8739 swipeMessage.find('.tokenCounterDisplay').text('');
8804 swipeMessage.find('.mes_reasoning').html('');8740 updateReasoningUI(swipeMessage, { reset: true });
8805 } else {8741 } else {
8806 //console.log('showing previously generated swipe candidate, or "..."');8742 //console.log('showing previously generated swipe candidate, or "..."');
8807 //console.log('onclick right swipe calling addOneMessage');8743 //console.log('onclick right swipe calling addOneMessage');
@@ -8851,7 +8787,6 @@ const swipe_right = () => {
8851 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {8787 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
8852 console.debug('caught here 2');8788 console.debug('caught here 2');
8853 is_send_press = true;8789 is_send_press = true;
8854 $('.mes_buttons:last').hide();
8855 await Generate('swipe');8790 await Generate('swipe');
8856 } else {8791 } else {
8857 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {8792 if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
public/scripts/openai.js+1 -2
@@ -83,7 +83,6 @@ export {
83 setOpenAIMessageExamples,83 setOpenAIMessageExamples,
84 setupChatCompletionPromptManager,84 setupChatCompletionPromptManager,
85 sendOpenAIRequest,85 sendOpenAIRequest,
86 getChatCompletionModel,
87 TokenHandler,86 TokenHandler,
88 IdentifierNotFoundError,87 IdentifierNotFoundError,
89 Message,88 Message,
@@ -1498,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
1498 }1497 }
1499}1498}
15001499
1501function getChatCompletionModel() {1500export function getChatCompletionModel() {
1502 switch (oai_settings.chat_completion_source) {1501 switch (oai_settings.chat_completion_source) {
1503 case chat_completion_sources.CLAUDE:1502 case chat_completion_sources.CLAUDE:
1504 return oai_settings.claude_model;1503 return oai_settings.claude_model;
public/scripts/power-user.js+1 -0
@@ -258,6 +258,7 @@ let power_user = {
258 auto_parse: false,258 auto_parse: false,
259 add_to_prompts: false,259 add_to_prompts: false,
260 auto_expand: false,260 auto_expand: false,
261 show_hidden: false,
261 prefix: '<think>\n',262 prefix: '<think>\n',
262 suffix: '\n</think>',263 suffix: '\n</think>',
263 separator: '\n\n',264 separator: '\n\n',
public/scripts/reasoning.js+350 -21
@@ -1,11 +1,11 @@
1import {1import {
2 moment,2 moment,
3} from '../lib.js';3} from '../lib.js';
4import { chat, closeMessageEditor, event_types, eventSource, main_api, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';4import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';5import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
6import { getCurrentLocale, t } from './i18n.js';6import { getCurrentLocale, t } from './i18n.js';
7import { MacrosParser } from './macros.js';7import { MacrosParser } from './macros.js';
8import { chat_completion_sources, oai_settings } from './openai.js';8import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
9import { Popup } from './popup.js';9import { Popup } from './popup.js';
10import { power_user } from './power-user.js';10import { power_user } from './power-user.js';
11import { SlashCommand } from './slash-commands/SlashCommand.js';11import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -13,7 +13,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
13import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';13import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
14import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';14import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
15import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';15import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
16import { copyText, escapeRegex, isFalseBoolean } from './utils.js';16import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js';
1717
18/**18/**
19 * Gets a message from a jQuery element.19 * Gets a message from a jQuery element.
@@ -71,21 +71,333 @@ export function extractReasoningFromData(data) {
71}71}
7272
73/**73/**
74 * Updates the Reasoning controls74 * Check if the model supports reasoning, but does not send back the reasoning
75 * @param {HTMLElement} element The element to update75 * @returns {boolean} True if the model supports reasoning
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 */76 */
80export function updateReasoningTimeUI(element, duration, { forceEnd = false } = {}) {77export function isHiddenReasoningModel() {
78 if (main_api !== 'openai') {
79 return false;
80 }
81
82 /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */
83 /** @type {Record.<string, MatchingFunc>} */
84 const FUNCS = {
85 equals: (currentModel, supportedModel) => currentModel === supportedModel,
86 startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel),
87 };
88
89 /** @type {{ name: string; func: MatchingFunc; }[]} */
90 const hiddenReasoningModels = [
91 { name: 'o1', func: FUNCS.startsWith },
92 { name: 'o3', func: FUNCS.startsWith },
93 { name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith },
94 { name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
95 ];
96
97 const model = getChatCompletionModel();
98
99 const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
100 return isHidden;
101}
102
103/**
104 * Updates the Reasoning UI for a specific message
105 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element
106 * @param {Object} [options={}] - Optional arguments
107 * @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping)
108 */
109export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) {
110 const handler = new ReasoningHandler();
111 handler.initHandleMessage(messageIdOrElement, { reset });
112}
113
114
115/**
116 * Enum for representing the state of reasoning
117 * @enum {string}
118 * @readonly
119 */
120export const ReasoningState = {
121 None: 'none',
122 Thinking: 'thinking',
123 Done: 'done',
124 Hidden: 'hidden',
125};
126
127/**
128 * Handles reasoning-specific logic and DOM updates for messages.
129 * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
130 */
131export class ReasoningHandler {
132 #isHiddenReasoningModel;
133
134 /**
135 * @param {Date?} [timeStarted=null] - When the generation started
136 */
137 constructor(timeStarted = null) {
138 /** @type {ReasoningState} The current state of the reasoning process */
139 this.state = ReasoningState.None;
140 /** @type {string} The reasoning output */
141 this.reasoning = '';
142 /** @type {Date} When the reasoning started */
143 this.startTime = null;
144 /** @type {Date} When the reasoning ended */
145 this.endTime = null;
146
147 /** @type {Date} Initial starting time of the generation */
148 this.initialTime = timeStarted ?? new Date();
149
150 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
151 this.#isHiddenReasoningModel = isHiddenReasoningModel();
152
153 // Cached DOM elements for reasoning
154 /** @type {HTMLElement} Main message DOM element `.mes` */
155 this.messageDom = null;
156 /** @type {HTMLElement} Reasoning details DOM element `.mes_reasoning_details` */
157 this.messageReasoningDetailsDom = null;
158 /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
159 this.messageReasoningContentDom = null;
160 /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
161 this.messageReasoningHeaderDom = null;
162 }
163
164 /**
165 * Initializes the reasoning handler for a specific message.
166 *
167 * Can be used to update the DOM elements or read other reasoning states.
168 * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
169 * The state will always be either done/hidden or none.
170 *
171 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
172 * @param {Object} [options={}] - Optional arguments
173 * @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping)
174 */
175 initHandleMessage(messageIdOrElement, { reset = false } = {}) {
176 /** @type {HTMLElement} */
177 const messageElement = typeof messageIdOrElement === 'number'
178 ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
179 : messageIdOrElement instanceof HTMLElement
180 ? messageIdOrElement
181 : $(messageIdOrElement)[0];
182 const messageId = Number(messageElement.getAttribute('mesid'));
183
184 if (isNaN(messageId)) return;
185
186 const extra = chat[messageId].extra;
187
188 if (extra.reasoning) {
189 this.state = ReasoningState.Done;
190 } else if (extra.reasoning_duration) {
191 this.state = ReasoningState.Hidden;
192 }
193
194 this.reasoning = extra?.reasoning ?? '';
195
196 if (this.state !== ReasoningState.None) {
197 this.initialTime = new Date(chat[messageId].gen_started);
198 this.startTime = this.initialTime;
199 this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
200 }
201
202 // Prefill main dom element, as message might not have been rendered yet
203 this.messageDom = messageElement;
204
205 // Make sure reset correctly clears all relevant states
206 if (reset) {
207 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
208 this.reasoning = '';
209 this.initialTime = new Date();
210 this.startTime = null;
211 this.endTime = null;
212 }
213
214 this.updateDom(messageId);
215 }
216
217 /**
218 * Gets the duration of the reasoning in milliseconds.
219 *
220 * @returns {number?} The duration in milliseconds, or null if the start or end time is not set
221 */
222 getDuration() {
223 if (this.startTime && this.endTime) {
224 return this.endTime.getTime() - this.startTime.getTime();
225 }
226 return null;
227 }
228
229 /**
230 * Updates the reasoning text/string for a message.
231 *
232 * @param {number} messageId - The ID of the message to update
233 * @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning
234 * @param {Object} [options={}] - Optional arguments
235 * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
236 * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
237 */
238 updateReasoning(messageId, reasoning = null, { persist = false } = {}) {
239 reasoning = reasoning ?? this.reasoning;
240 reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning;
241
242 // Ensure the chat extra exists
243 if (!chat[messageId].extra) {
244 chat[messageId].extra = {};
245 }
246 const extra = chat[messageId].extra;
247
248 const reasoningChanged = extra.reasoning !== reasoning;
249 this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
250
251 if (persist) {
252 // Build and save the reasoning data to message extras
253 extra.reasoning = this.reasoning;
254 extra.reasoning_duration = this.getDuration();
255 }
256
257 return reasoningChanged;
258 }
259
260
261 /**
262 * Handles processing of reasoning for a message.
263 *
264 * This is usually called by the message processor when a message is changed.
265 *
266 * @param {number} messageId - The ID of the message to process
267 * @param {boolean} mesChanged - Whether the message has changed
268 * @returns {Promise<void>}
269 */
270 async process(messageId, mesChanged) {
271 if (!this.reasoning && !this.#isHiddenReasoningModel) return;
272
273 // Ensure reasoning string is updated and regexes are applied correctly
274 const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
275
276 if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
277 this.state = ReasoningState.Thinking;
278 this.startTime = this.initialTime;
279 }
280 if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
281 this.endTime = new Date();
282 await this.finish(messageId);
283 }
284 }
285
286 /**
287 * Completes the reasoning process for a message.
288 *
289 * Records the finish time if it was not set during streaming and updates the reasoning state.
290 * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
291 *
292 * @param {number} messageId - The ID of the message to complete reasoning for
293 * @returns {Promise<void>}
294 */
295 async finish(messageId) {
296 if (this.state === ReasoningState.None) return;
297
298 // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
299 if (this.startTime !== null && this.endTime === null) {
300 this.endTime = new Date();
301 }
302
303 if (this.state === ReasoningState.Thinking) {
304 this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
305 this.updateReasoning(messageId, null, { persist: true });
306 await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
307 }
308
309 this.updateDom(messageId);
310 }
311
312 /**
313 * Updates the reasoning UI elements for a message.
314 *
315 * Toggles the CSS class, updates states, reasoning message, and duration.
316 *
317 * @param {number} messageId - The ID of the message to update
318 */
319 updateDom(messageId) {
320 this.#checkDomElements(messageId);
321
322 // Main CSS class to show this message includes reasoning
323 this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
324
325 // Update states to the relevant DOM elements
326 setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
327 setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
328
329 // Update the reasoning message
330 const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
331 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
332 this.messageReasoningContentDom.innerHTML = displayReasoning;
333
334 // Update tooltip for hidden reasoning edit
335 /** @type {HTMLElement} */
336 const button = this.messageDom.querySelector('.mes_edit_add_reasoning');
337 button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`;
338
339 // Update the reasoning duration in the UI
340 this.#updateReasoningTimeUI();
341 }
342
343 /**
344 * Finds and caches reasoning-related DOM elements for the given message.
345 *
346 * @param {number} messageId - The ID of the message to cache the DOM elements for
347 */
348 #checkDomElements(messageId) {
349 // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
350 if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
351 this.messageDom = null;
352 }
353
354 // Cache the DOM elements once
355 if (this.messageDom === null) {
356 this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
357 if (this.messageDom === null) throw new Error('message dom does not exist');
358 }
359 if (this.messageReasoningDetailsDom === null) {
360 this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
361 }
362 if (this.messageReasoningContentDom === null) {
363 this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
364 }
365 if (this.messageReasoningHeaderDom === null) {
366 this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
367 }
368 }
369
370 /**
371 * Updates the reasoning time display in the UI.
372 *
373 * Shows the duration in a human-readable format with a tooltip for exact seconds.
374 * Displays "Thinking..." if still processing, or a generic message otherwise.
375 */
376 #updateReasoningTimeUI() {
377 const element = this.messageReasoningHeaderDom;
378 const duration = this.getDuration();
379 let data = null;
81 if (duration) {380 if (duration) {
82 const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });381 const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
83 const secondsStr = moment.duration(duration).asSeconds();382 const secondsStr = moment.duration(duration).asSeconds();
84 element.innerHTML = t`Thought for <span title="${secondsStr} seconds">${durationStr}</span>`;383
85 } else if (forceEnd) {384 const span = document.createElement('span');
385 span.title = t`${secondsStr} seconds`;
386 span.textContent = durationStr;
387
388 element.textContent = t`Thought for `;
389 element.appendChild(span);
390 data = String(secondsStr);
391 } else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
86 element.textContent = t`Thought for some time`;392 element.textContent = t`Thought for some time`;
393 data = 'unknown';
87 } else {394 } else {
88 element.textContent = t`Thinking...`;395 element.textContent = t`Thinking...`;
396 data = null;
397 }
398
399 setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
400 setDatasetProperty(element, 'duration', data);
89 }401 }
90}402}
91403
@@ -95,7 +407,6 @@ export function updateReasoningTimeUI(element, duration, { forceEnd = false } =
95 */407 */
96export class PromptReasoning {408export class PromptReasoning {
97 static REASONING_PLACEHOLDER = '\u200B';409 static REASONING_PLACEHOLDER = '\u200B';
98 static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
99410
100 constructor() {411 constructor() {
101 this.counter = 0;412 this.counter = 0;
@@ -126,7 +437,7 @@ export class PromptReasoning {
126 return content;437 return content;
127 }438 }
128439
129 // No reasoning provided or a placeholder440 // No reasoning provided or a legacy placeholder
130 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {441 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
131 return content;442 return content;
132 }443 }
@@ -192,6 +503,15 @@ function loadReasoningSettings() {
192 toggleReasoningAutoExpand();503 toggleReasoningAutoExpand();
193 saveSettingsDebounced();504 saveSettingsDebounced();
194 });505 });
506 toggleReasoningAutoExpand();
507
508 $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
509 $('#reasoning_show_hidden').on('change', function () {
510 power_user.reasoning.show_hidden = !!$(this).prop('checked');
511 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
512 saveSettingsDebounced();
513 });
514 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
195}515}
196516
197function registerReasoningSlashCommands() {517function registerReasoningSlashCommands() {
@@ -211,7 +531,7 @@ function registerReasoningSlashCommands() {
211 const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;531 const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;
212 const message = chat[messageId];532 const message = chat[messageId];
213 const reasoning = String(message?.extra?.reasoning ?? '');533 const reasoning = String(message?.extra?.reasoning ?? '');
214 return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');534 return reasoning;
215 },535 },
216 }));536 }));
217537
@@ -338,7 +658,7 @@ function setReasoningEventHandlers() {
338 const textarea = document.createElement('textarea');658 const textarea = document.createElement('textarea');
339 const reasoningBlock = messageBlock.find('.mes_reasoning');659 const reasoningBlock = messageBlock.find('.mes_reasoning');
340 textarea.classList.add('reasoning_edit_textarea');660 textarea.classList.add('reasoning_edit_textarea');
341 textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');661 textarea.value = reasoning;
342 $(textarea).insertBefore(reasoningBlock);662 $(textarea).insertBefore(reasoningBlock);
343663
344 if (!CSS.supports('field-sizing', 'content')) {664 if (!CSS.supports('field-sizing', 'content')) {
@@ -393,10 +713,12 @@ function setReasoningEventHandlers() {
393 textarea.remove();713 textarea.remove();
394714
395 messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');715 messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
716
717 updateReasoningUI(messageBlock);
396 });718 });
397719
398 $(document).on('click', '.mes_edit_add_reasoning', async function () {720 $(document).on('click', '.mes_edit_add_reasoning', async function () {
399 const { message, messageId, messageBlock } = getMessageFromJquery(this);721 const { message, messageBlock } = getMessageFromJquery(this);
400 if (!message?.extra) {722 if (!message?.extra) {
401 return;723 return;
402 }724 }
@@ -406,8 +728,16 @@ function setReasoningEventHandlers() {
406 return;728 return;
407 }729 }
408730
409 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;731 messageBlock.addClass('reasoning');
410 updateMessageBlock(messageId, message, { rerenderMessage: false });732
733 // To make hidden reasoning blocks editable, we just set them to "Done" here already.
734 // They will be done on save anyway - and on cancel the reasoning block gets rerendered too.
735 if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) {
736 messageBlock.attr('data-reasoning-state', ReasoningState.Done);
737 }
738
739 // Open the reasoning area so we can actually edit it
740 messageBlock.find('.mes_reasoning_details').attr('open', '');
411 messageBlock.find('.mes_reasoning_edit').trigger('click');741 messageBlock.find('.mes_reasoning_edit').trigger('click');
412 await saveChatConditional();742 await saveChatConditional();
413 });743 });
@@ -416,7 +746,7 @@ function setReasoningEventHandlers() {
416 e.stopPropagation();746 e.stopPropagation();
417 e.preventDefault();747 e.preventDefault();
418748
419 const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);749 const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.`);
420750
421 if (!confirm) {751 if (!confirm) {
422 return;752 return;
@@ -435,7 +765,7 @@ function setReasoningEventHandlers() {
435765
436 $(document).on('pointerup', '.mes_reasoning_copy', async function () {766 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
437 const { message } = getMessageFromJquery(this);767 const { message } = getMessageFromJquery(this);
438 const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');768 const reasoning = String(message?.extra?.reasoning ?? '');
439769
440 if (!reasoning) {770 if (!reasoning) {
441 return;771 return;
@@ -553,7 +883,6 @@ function registerReasoningAppEvents() {
553883
554export function initReasoning() {884export function initReasoning() {
555 loadReasoningSettings();885 loadReasoningSettings();
556 toggleReasoningAutoExpand();
557 setReasoningEventHandlers();886 setReasoningEventHandlers();
558 registerReasoningSlashCommands();887 registerReasoningSlashCommands();
559 registerReasoningMacros();888 registerReasoningMacros();
public/scripts/utils.js+17 -0
@@ -2059,6 +2059,23 @@ export function toggleDrawer(drawer, expand = true) {
2059 }2059 }
2060}2060}
20612061
2062/**
2063 * Sets or removes a dataset property on an HTMLElement
2064 *
2065 * Utility function to make it easier to reset dataset properties on null, without them being "null" as value.
2066 *
2067 * @param {HTMLElement} element - The element to modify
2068 * @param {string} name - The name of the dataset property
2069 * @param {string|null} value - The value to set - If null, the dataset property will be removed
2070 */
2071export function setDatasetProperty(element, name, value) {
2072 if (value === null) {
2073 delete element.dataset[name];
2074 } else {
2075 element.dataset[name] = value;
2076 }
2077}
2078
2062export async function fetchFaFile(name) {2079export async function fetchFaFile(name) {
2063 const style = document.createElement('style');2080 const style = document.createElement('style');
2064 style.innerHTML = await (await fetch(`/css/${name}`)).text();2081 style.innerHTML = await (await fetch(`/css/${name}`)).text();
public/style.css+21 -5
@@ -410,14 +410,26 @@ input[type='checkbox']:focus-visible {
410}410}
411411
412.mes_bias:empty,412.mes_bias:empty,
413.mes_reasoning:empty,413.mes:not(.reasoning) .mes_reasoning_details,
414.mes_reasoning_details:has(.mes_reasoning:empty),
415.mes_reasoning_details:not([open]) .mes_reasoning_actions,414.mes_reasoning_details:not([open]) .mes_reasoning_actions,
416.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,415.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
417.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,416.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
418.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
419.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),417.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
420.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions {418.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
419.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
420.mes.reasoning:not([data-reasoning-state="hidden"]) .mes_edit_add_reasoning,
421.mes:has(.mes_reasoning:empty) .mes_reasoning_arrow,
422.mes:has(.mes_reasoning:empty) .mes_reasoning,
423.mes:has(.mes_reasoning:empty) .mes_reasoning_copy {
424 display: none;
425}
426
427.mes[data-reasoning-state="hidden"] .mes_edit_add_reasoning {
428 background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%);
429}
430
431/** If hidden reasoning should not be shown, we hide all blocks that don't have content */
432#chat:not([data-show-hidden-reasoning="true"]) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
421 display: none;433 display: none;
422}434}
423435
@@ -4319,7 +4331,11 @@ input[type="range"]::-webkit-slider-thumb {
4319 field-sizing: content;4331 field-sizing: content;
4320}4332}
43214333
4322.mes.reasoning .mes_edit_add_reasoning {4334body[data-generating="true"] #send_but,
4335body[data-generating="true"] #mes_continue,
4336body[data-generating="true"] #mes_impersonate,
4337body[data-generating="true"] #chat .last_mes .mes_buttons,
4338body[data-generating="true"] #chat .last_mes .mes_reasoning_actions {
4323 display: none;4339 display: none;
4324}4340}
43254341
src/endpoints/backends/chat-completions.js+0 -7
@@ -288,7 +288,6 @@ async function sendMakerSuiteRequest(request, response) {
288288
289 const model = String(request.body.model);289 const model = String(request.body.model);
290 const stream = Boolean(request.body.stream);290 const stream = Boolean(request.body.stream);
291 const showThoughts = Boolean(request.body.include_reasoning);
292 const isThinking = model.includes('thinking');291 const isThinking = model.includes('thinking');
293292
294 const generationConfig = {293 const generationConfig = {
@@ -337,12 +336,6 @@ async function sendMakerSuiteRequest(request, response) {
337 body.systemInstruction = prompt.system_instruction;336 body.systemInstruction = prompt.system_instruction;
338 }337 }
339338
340 if (isThinking && showThoughts) {
341 generationConfig.thinkingConfig = {
342 includeThoughts: true,
343 };
344 }
345
346 return body;339 return body;
347 }340 }
348341