Merge pull request #3444 from SillyTavern/hidden-reasoning-tracking Track reasoning duration for models with hidden reasoning
Signed| @@ -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-Expand | 3810 | 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"> |
| @@ -269,7 +269,7 @@ import { initSettingsSearch } from './scripts/setting-search.js'; | |||
| 269 | import { initBulkEdit } from './scripts/bulk-edit.js'; | 269 | import { initBulkEdit } from './scripts/bulk-edit.js'; |
| 270 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; | 270 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; |
| 271 | import { getContext } from './scripts/st-context.js'; | 271 | import { getContext } from './scripts/st-context.js'; |
| 272 | import { extractReasoningFromData, initReasoning, PromptReasoning, removeReasoningFromString, updateReasoningTimeUI } from './scripts/reasoning.js'; | 272 | import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; |
| 273 | import { accountStorage } from './scripts/util/AccountStorage.js'; | 273 | import { accountStorage } from './scripts/util/AccountStorage.js'; |
| 274 | 274 | ||
| 275 | // API OBJECT FOR EXTERNAL WIRING | 275 | // 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); |
| 2260 | 2257 | ||
| 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 | } | ||
| 2267 | 2259 | ||
| 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 ?? ''; |
| 2451 | 2443 | ||
| 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 | }; |
| 2470 | 2460 | ||
| 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 started | 2575 | * @param {Date} gen_started Date when generation was started |
| 2586 | * @param {Date} gen_finished Date when generation was finished | 2576 | * @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 title | 2579 | * @returns {Object} Object containing the formatted timer value and title |
| 2589 | * @example | 2580 | * @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.2s | 2582 | * 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/s | 2583 | * 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 | */ |
| 2594 | function formatGenerationTimer(gen_started, gen_finished, tokenCount) { | 2585 | function 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(); |
| 2610 | 2602 | ||
| 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 | } |
| 3163 | 3146 | ||
| 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 | } |
| 3174 | 3156 | ||
| 3175 | #updateMessageBlockVisibility() { | 3157 | #updateMessageBlockVisibility() { |
| @@ -3179,22 +3161,12 @@ class StreamingProcessor { | |||
| 3179 | } | 3161 | } |
| 3180 | } | 3162 | } |
| 3181 | 3163 | ||
| 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 | } | 3166 | } |
| 3190 | 3167 | ||
| 3191 | hideMessageButtons(messageId) { | 3168 | markUIGenStopped() { |
| 3192 | if (messageId == -1) { | 3169 | activateSendButtons(); |
| 3193 | return; | ||
| 3194 | } | ||
| 3195 | |||
| 3196 | hideStopButton(); | ||
| 3197 | $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' }); | ||
| 3198 | } | 3170 | } |
| 3199 | 3171 | ||
| 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 { | |||
| 3228 | 3198 | ||
| 3229 | let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings); | 3199 | let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings); |
| 3230 | 3200 | ||
| 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 | } |
| 3258 | 3223 | ||
| 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); | ||
| 3271 | 3226 | ||
| 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 | } |
| 3309 | 3254 | ||
| 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 | } |
| 3323 | 3268 | ||
| 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}"]`)); |
| 3335 | 3273 | ||
| 3336 | // Ensure reasoning finish time is recorded if not already | 3274 | 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 | } | ||
| 3341 | 3275 | ||
| 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; |
| 3380 | 3314 | ||
| 3381 | this.hideMessageButtons(this.messageId); | 3315 | this.markUIGenStopped(); |
| 3382 | generatedPromptCache = ''; | 3316 | generatedPromptCache = ''; |
| 3383 | unblockGeneration(); | 3317 | unblockGeneration(); |
| 3384 | 3318 | ||
| @@ -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 | } |
| 6197 | 6132 | ||
| 6133 | /** | ||
| 6134 | * A function mainly used to switch 'generating' state - setting it to false and activating the buttons again | ||
| 6135 | */ | ||
| 6198 | export function activateSendButtons() { | 6136 | export 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 | } |
| 6206 | 6141 | ||
| 6142 | /** | ||
| 6143 | * A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons | ||
| 6144 | */ | ||
| 6207 | export function deactivateSendButtons() { | 6145 | export 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 | } |
| 6213 | 6149 | ||
| 6214 | export function resetChatState() { | 6150 | export function resetChatState() { |
| @@ -8801,7 +8737,7 @@ const swipe_right = () => { | |||
| 8801 | // resets the timer | 8737 | // 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) { |
| @@ -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 | } |
| 1500 | 1499 | ||
| 1501 | function getChatCompletionModel() { | 1500 | export 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; |
| @@ -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', |
| @@ -1,11 +1,11 @@ | |||
| 1 | import { | 1 | import { |
| 2 | moment, | 2 | moment, |
| 3 | } from '../lib.js'; | 3 | } from '../lib.js'; |
| 4 | import { chat, closeMessageEditor, event_types, eventSource, main_api, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; | 4 | import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; |
| 5 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; | 5 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 6 | import { getCurrentLocale, t } from './i18n.js'; | 6 | import { getCurrentLocale, t } from './i18n.js'; |
| 7 | import { MacrosParser } from './macros.js'; | 7 | import { MacrosParser } from './macros.js'; |
| 8 | import { chat_completion_sources, oai_settings } from './openai.js'; | 8 | import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js'; |
| 9 | import { Popup } from './popup.js'; | 9 | import { Popup } from './popup.js'; |
| 10 | import { power_user } from './power-user.js'; | 10 | import { power_user } from './power-user.js'; |
| 11 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 11 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| @@ -13,7 +13,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' | |||
| 13 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 13 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 14 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 15 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; | 15 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 16 | import { copyText, escapeRegex, isFalseBoolean } from './utils.js'; | 16 | import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js'; |
| 17 | 17 | ||
| 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 | } |
| 72 | 72 | ||
| 73 | /** | 73 | /** |
| 74 | * Updates the Reasoning controls | 74 | * Check if the model supports reasoning, but does not send back the reasoning |
| 75 | * @param {HTMLElement} element The element to update | 75 | * @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 | */ |
| 80 | export function updateReasoningTimeUI(element, duration, { forceEnd = false } = {}) { | 77 | export function isHiddenReasoningModel() { |
| 81 | if (duration) { | 78 | if (main_api !== 'openai') { |
| 82 | const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 }); | 79 | return false; |
| 83 | const secondsStr = moment.duration(duration).asSeconds(); | 80 | } |
| 84 | element.innerHTML = t`Thought for <span title="${secondsStr} seconds">${durationStr}</span>`; | 81 | |
| 85 | } else if (forceEnd) { | 82 | /** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */ |
| 86 | element.textContent = t`Thought for some time`; | 83 | /** @type {Record.<string, MatchingFunc>} */ |
| 87 | } else { | 84 | const FUNCS = { |
| 88 | element.textContent = t`Thinking...`; | 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 | */ | ||
| 109 | export 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 | */ | ||
| 120 | export 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 | */ | ||
| 131 | export 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; | ||
| 380 | if (duration) { | ||
| 381 | const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 }); | ||
| 382 | const secondsStr = moment.duration(duration).asSeconds(); | ||
| 383 | |||
| 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)) { | ||
| 392 | element.textContent = t`Thought for some time`; | ||
| 393 | data = 'unknown'; | ||
| 394 | } else { | ||
| 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 | } |
| 91 | 403 | ||
| @@ -95,7 +407,6 @@ export function updateReasoningTimeUI(element, duration, { forceEnd = false } = | |||
| 95 | */ | 407 | */ |
| 96 | export class PromptReasoning { | 408 | export class PromptReasoning { |
| 97 | static REASONING_PLACEHOLDER = '\u200B'; | 409 | static REASONING_PLACEHOLDER = '\u200B'; |
| 98 | static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`); | ||
| 99 | 410 | ||
| 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 | } |
| 128 | 439 | ||
| 129 | // No reasoning provided or a placeholder | 440 | // 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 | } |
| 196 | 516 | ||
| 197 | function registerReasoningSlashCommands() { | 517 | function 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 | })); |
| 217 | 537 | ||
| @@ -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); |
| 343 | 663 | ||
| 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(); |
| 394 | 714 | ||
| 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 | }); |
| 397 | 719 | ||
| 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 | } |
| 408 | 730 | ||
| 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(); |
| 418 | 748 | ||
| 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.`); |
| 420 | 750 | ||
| 421 | if (!confirm) { | 751 | if (!confirm) { |
| 422 | return; | 752 | return; |
| @@ -435,7 +765,7 @@ function setReasoningEventHandlers() { | |||
| 435 | 765 | ||
| 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 ?? ''); |
| 439 | 769 | ||
| 440 | if (!reasoning) { | 770 | if (!reasoning) { |
| 441 | return; | 771 | return; |
| @@ -553,7 +883,6 @@ function registerReasoningAppEvents() { | |||
| 553 | 883 | ||
| 554 | export function initReasoning() { | 884 | export function initReasoning() { |
| 555 | loadReasoningSettings(); | 885 | loadReasoningSettings(); |
| 556 | toggleReasoningAutoExpand(); | ||
| 557 | setReasoningEventHandlers(); | 886 | setReasoningEventHandlers(); |
| 558 | registerReasoningSlashCommands(); | 887 | registerReasoningSlashCommands(); |
| 559 | registerReasoningMacros(); | 888 | registerReasoningMacros(); |
| @@ -2059,6 +2059,23 @@ export function toggleDrawer(drawer, expand = true) { | |||
| 2059 | } | 2059 | } |
| 2060 | } | 2060 | } |
| 2061 | 2061 | ||
| 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 | */ | ||
| 2071 | export function setDatasetProperty(element, name, value) { | ||
| 2072 | if (value === null) { | ||
| 2073 | delete element.dataset[name]; | ||
| 2074 | } else { | ||
| 2075 | element.dataset[name] = value; | ||
| 2076 | } | ||
| 2077 | } | ||
| 2078 | |||
| 2062 | export async function fetchFaFile(name) { | 2079 | export 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(); |
| @@ -410,14 +410,26 @@ input[type='checkbox']:focus-visible { | |||
| 410 | } | 410 | } |
| 411 | 411 | ||
| 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 | } |
| 423 | 435 | ||
| @@ -4319,7 +4331,11 @@ input[type="range"]::-webkit-slider-thumb { | |||
| 4319 | field-sizing: content; | 4331 | field-sizing: content; |
| 4320 | } | 4332 | } |
| 4321 | 4333 | ||
| 4322 | .mes.reasoning .mes_edit_add_reasoning { | 4334 | body[data-generating="true"] #send_but, |
| 4335 | body[data-generating="true"] #mes_continue, | ||
| 4336 | body[data-generating="true"] #mes_impersonate, | ||
| 4337 | body[data-generating="true"] #chat .last_mes .mes_buttons, | ||
| 4338 | body[data-generating="true"] #chat .last_mes .mes_reasoning_actions { | ||
| 4323 | display: none; | 4339 | display: none; |
| 4324 | } | 4340 | } |
| 4325 | 4341 | ||
| @@ -288,7 +288,6 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 288 | 288 | ||
| 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'); |
| 293 | 292 | ||
| 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 | } |
| 339 | 338 | ||
| 340 | if (isThinking && showThoughts) { | ||
| 341 | generationConfig.thinkingConfig = { | ||
| 342 | includeThoughts: true, | ||
| 343 | }; | ||
| 344 | } | ||
| 345 | |||
| 346 | return body; | 339 | return body; |
| 347 | } | 340 | } |
| 348 | 341 | ||