Merge pull request #3444 from SillyTavern/hidden-reasoning-tracking Track reasoning duration for models with hidden reasoning
Signed| @@ -1983,12 +1983,12 @@ | ||
| 1983 | 1983 | </span> |
| 1984 | 1984 | </div> |
| 1985 | 1985 | </div> |
| 1986 | 1986 | <div class="range-block" data-source="makersuite,deepseek,openrouter"> |
| 1987 | 1987 | <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand"> |
| 1988 | 1988 | <input id="openai_show_thoughts" type="checkbox" /> |
| 1989 | 1989 | <span> |
| 1990 | 1990 | <span data-i18n="Request model reasoning">Request model reasoning</span> |
| 1991 | 1991 | <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i> |
| 1992 | 1992 | </span> |
| 1993 | 1993 | </label> |
| 1994 | 1994 | <div class="toggle-description justifyLeft marginBot5"> |
| @@ -3810,6 +3810,12 @@ | ||
| 3810 | 3810 | Auto-Expand |
| 3811 | 3811 | </small> |
| 3812 | 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 | 3819 | </div> |
| 3814 | 3820 | <div class="flex-container alignItemsBaseline"> |
| 3815 | 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 | 269 | import { initBulkEdit } from './scripts/bulk-edit.js'; |
| 270 | 270 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; |
| 271 | 271 | import { getContext } from './scripts/st-context.js'; |
| 272 | 272 | import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningTimeUIupdateReasoningUI } from './scripts/reasoning.js'; |
| 273 | 273 | import { accountStorage } from './scripts/util/AccountStorage.js'; |
| 274 | 274 | |
| 275 | 275 | // API OBJECT FOR EXTERNAL WIRING |
| @@ -2224,8 +2224,6 @@ function getMessageFromTemplate({ | ||
| 2224 | 2224 | isUser, |
| 2225 | 2225 | avatarImg, |
| 2226 | 2226 | bias, |
| 2227 | - reasoning, | |
| 2228 | - reasoningDuration, | |
| 2229 | 2227 | isSystem, |
| 2230 | 2228 | title, |
| 2231 | 2229 | timerValue, |
| @@ -2250,7 +2248,6 @@ function getMessageFromTemplate({ | ||
| 2250 | 2248 | mes.find('.avatar img').attr('src', avatarImg); |
| 2251 | 2249 | mes.find('.ch_name .name_text').text(characterName); |
| 2252 | 2250 | mes.find('.mes_bias').html(bias); |
| 2253 | - mes.find('.mes_reasoning').html(reasoning); | |
| 2254 | 2251 | mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`); |
| 2255 | 2252 | mes.find('.mesIDDisplay').text(`#${mesId}`); |
| 2256 | 2253 | tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`); |
| @@ -2258,12 +2255,7 @@ function getMessageFromTemplate({ | ||
| 2258 | 2255 | timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue); |
| 2259 | 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 | 2260 | if (power_user.timestamp_model_icon && extra?.api) { |
| 2269 | 2261 | insertSVGIcon(mes, extra); |
| @@ -2285,8 +2277,9 @@ export function updateMessageBlock(messageId, message, { rerenderMessage = true | ||
| 2285 | 2277 | const text = message?.extra?.display_text ?? message.mes; |
| 2286 | 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 | 2283 | addCopyToCodeBlocks(messageElement); |
| 2291 | 2284 | appendMediaToMessage(message, messageElement); |
| 2292 | 2285 | } |
| @@ -2446,7 +2439,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2446 | 2439 | false, |
| 2447 | 2440 | ); |
| 2448 | 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 | 2442 | let bookmarkLink = mes?.extra?.bookmark_link ?? ''; |
| 2451 | 2443 | |
| 2452 | 2444 | let params = { |
| @@ -2456,8 +2448,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2456 | 2448 | isUser: mes.is_user, |
| 2457 | 2449 | avatarImg: avatarImg, |
| 2458 | 2450 | bias: bias, |
| 2459 | - reasoning: reasoning, | |
| 2460 | - reasoningDuration: mes.extra?.reasoning_duration, | |
| 2461 | 2451 | isSystem: isSystem, |
| 2462 | 2452 | title: title, |
| 2463 | 2453 | bookmarkLink: bookmarkLink, |
| @@ -2465,7 +2455,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2465 | 2455 | timestamp: timestamp, |
| 2466 | 2456 | extra: mes.extra, |
| 2467 | 2457 | tokenCount: mes.extra?.token_count ?? 0, |
| 2468 | 2458 | ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration), |
| 2469 | 2459 | }; |
| 2470 | 2460 | |
| 2471 | 2461 | const renderedMessage = getMessageFromTemplate(params); |
| @@ -2517,8 +2507,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2517 | 2507 | const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`); |
| 2518 | 2508 | swipeMessage.attr('swipeid', params.swipeId); |
| 2519 | 2509 | swipeMessage.find('.mes_text').html(messageText).attr('title', title); |
| 2520 | - swipeMessage.find('.mes_reasoning').html(reasoning); | |
| 2521 | 2510 | swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`); |
| 2511 | + updateReasoningUI(swipeMessage); | |
| 2522 | 2512 | appendMediaToMessage(mes, swipeMessage); |
| 2523 | 2513 | if (power_user.timestamp_model_icon && params.extra?.api) { |
| 2524 | 2514 | insertSVGIcon(swipeMessage, params.extra); |
| @@ -2585,13 +2575,14 @@ export function formatCharacterAvatar(characterAvatar) { | ||
| 2585 | 2575 | * @param {Date} gen_started Date when generation was started |
| 2586 | 2576 | * @param {Date} gen_finished Date when generation was finished |
| 2587 | 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 | 2579 | * @returns {Object} Object containing the formatted timer value and title |
| 2589 | 2580 | * @example |
| 2590 | 2581 | * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount); |
| 2591 | 2582 | * console.log(timerValue); // 1.2s |
| 2592 | 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 | 2585 | function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) { |
| 2595 | 2586 | if (!gen_started || !gen_finished) { |
| 2596 | 2587 | return {}; |
| 2597 | 2588 | } |
| @@ -2605,8 +2596,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) { | ||
| 2605 | 2596 | `Generation queued: ${start.format(dateFormat)}`, |
| 2606 | 2597 | `Reply received: ${finish.format(dateFormat)}`, |
| 2607 | 2598 | `Time to generate: ${seconds} seconds`, |
| 2599 | + reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '', | |
| 2608 | 2600 | tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '', |
| 2609 | 2601 | ].filter(x => x).join('\n').trim(); |
| 2610 | 2602 | |
| 2611 | 2603 | if (isNaN(seconds) || seconds < 0) { |
| 2612 | 2604 | return { timerValue: '', timerTitle }; |
| @@ -3125,15 +3117,14 @@ class StreamingProcessor { | ||
| 3125 | 3117 | constructor(type, forceName2, timeStarted, continueMessage) { |
| 3126 | 3118 | this.result = ''; |
| 3127 | 3119 | this.messageId = -1; |
| 3120 | + /** @type {HTMLElement} */ | |
| 3128 | 3121 | this.messageDom = null; |
| 3122 | + /** @type {HTMLElement} */ | |
| 3129 | 3123 | this.messageTextDom = null; |
| 3124 | + /** @type {HTMLElement} */ | |
| 3130 | 3125 | this.messageTimerDom = null; |
| 3131 | 3126 | /** @type {HTMLElement} */ |
| 3132 | 3127 | this.messageTokenCounterDom = null; |
| 3133 | - /** @type {HTMLElement} */ | |
| 3134 | - this.messageReasoningDom = null; | |
| 3135 | - /** @type {HTMLElement} */ | |
| 3136 | - this.messageReasoningHeaderDom = null; | |
| 3137 | 3128 | /** @type {HTMLTextAreaElement} */ |
| 3138 | 3129 | this.sendTextarea = document.querySelector('#send_textarea'); |
| 3139 | 3130 | this.type = type; |
| @@ -3149,16 +3140,8 @@ class StreamingProcessor { | ||
| 3149 | 3140 | /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */ |
| 3150 | 3141 | this.messageLogprobs = []; |
| 3151 | 3142 | this.toolCalls = []; |
| 3152 | - this.reasoning = ''; | |
| 3143 | + // Initialize reasoning in its own handler | |
| 3153 | 3144 | this.reasoningStartTimereasoningHandler = nullnew 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 | 3147 | #checkDomElements(messageId) { |
| @@ -3167,9 +3150,8 @@ class StreamingProcessor { | ||
| 3167 | 3150 | this.messageTextDom = this.messageDom?.querySelector('.mes_text'); |
| 3168 | 3151 | this.messageTimerDom = this.messageDom?.querySelector('.mes_timer'); |
| 3169 | 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 | 3157 | #updateMessageBlockVisibility() { |
| @@ -3179,22 +3161,12 @@ class StreamingProcessor { | ||
| 3179 | 3161 | } |
| 3180 | 3162 | } |
| 3181 | 3163 | |
| 3182 | 3164 | showMessageButtonsmarkUIGenStarted(messageId) { |
| 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 | } |
| 3195 | 3167 | |
| 3196 | - hideStopButton(); | |
| 3168 | + markUIGenStopped() { | |
| 3197 | - $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' }); | |
| 3169 | + activateSendButtons(); | |
| 3198 | 3170 | } |
| 3199 | 3171 | |
| 3200 | 3172 | async onStartStreaming(text) { |
| @@ -3203,14 +3175,12 @@ class StreamingProcessor { | ||
| 3203 | 3175 | if (this.type == 'impersonate') { |
| 3204 | 3176 | this.sendTextarea.value = ''; |
| 3205 | 3177 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3206 | 3178 | } else { |
| 3207 | - else { | |
| 3208 | 3179 | await saveReply(this.type, text, true, '', [], ''); |
| 3209 | 3180 | messageId = chat.length - 1; |
| 3210 | 3181 | this.#checkDomElements(messageId); |
| 3211 | 3182 | this.showMessageButtonsmarkUIGenStarted(messageId); |
| 3212 | 3183 | } |
| 3213 | - | |
| 3214 | 3184 | hideSwipeButtons(); |
| 3215 | 3185 | scrollChatToBottom(); |
| 3216 | 3186 | return messageId; |
| @@ -3228,11 +3198,9 @@ class StreamingProcessor { | ||
| 3228 | 3198 | |
| 3229 | 3199 | let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings); |
| 3230 | 3200 | |
| 3231 | - // Predict unbalanced asterisks / quotes during streaming | |
| 3232 | 3201 | const charsToBalance = ['*', '"', '```']; |
| 3233 | 3202 | for (const char of charsToBalance) { |
| 3234 | 3203 | if (!isFinal && isOdd(countOccurrences(processedText, char))) { |
| 3235 | - // Add character at the end to balance it | |
| 3236 | 3204 | const separator = char.length > 1 ? '\n' : ''; |
| 3237 | 3205 | processedText = processedText.trimEnd() + separator + char; |
| 3238 | 3206 | } |
| @@ -3241,47 +3209,24 @@ class StreamingProcessor { | ||
| 3241 | 3209 | if (isImpersonate) { |
| 3242 | 3210 | this.sendTextarea.value = processedText; |
| 3243 | 3211 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3244 | 3212 | } else { |
| 3245 | - else { | |
| 3246 | 3213 | const mesChanged = chat[messageId]['mes'] !== processedText; |
| 3247 | - | |
| 3248 | 3214 | this.#checkDomElements(messageId); |
| 3249 | 3215 | this.#updateMessageBlockVisibility(); |
| 3250 | 3216 | const currentTime = new Date(); |
| 3251 | 3217 | chat[messageId]['mes'] = processedText; |
| 3252 | 3218 | chat[messageId]['gen_started'] = this.timeStarted; |
| 3253 | 3219 | chat[messageId]['gen_finished'] = currentTime; |
| 3254 | - | |
| 3255 | 3220 | if (!chat[messageId]['extra']) { |
| 3256 | 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 | 3229 | const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0; |
| 3284 | - | |
| 3285 | 3230 | if (currentTokenCount) { |
| 3286 | 3231 | chat[messageId]['extra']['token_count'] = currentTokenCount; |
| 3287 | 3232 | if (this.messageTokenCounterDom instanceof HTMLElement) { |
| @@ -3307,7 +3252,7 @@ class StreamingProcessor { | ||
| 3307 | 3252 | this.messageTextDom.innerHTML = formattedText; |
| 3308 | 3253 | } |
| 3309 | 3254 | |
| 3310 | 3255 | const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration()); |
| 3311 | 3256 | if (this.messageTimerDom instanceof HTMLElement) { |
| 3312 | 3257 | this.messageTimerDom.textContent = timePassed.timerValue; |
| 3313 | 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 | 3269 | async onFinishStreaming(messageId, text) { |
| 3332 | 3270 | this.hideMessageButtonsmarkUIGenStopped(this.messageId); |
| 3333 | 3271 | await this.onProgressStreaming(messageId, text, true); |
| 3334 | 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 | 3276 | if (Array.isArray(this.swipes) && this.swipes.length > 0) { |
| 3343 | 3277 | const message = chat[messageId]; |
| @@ -3378,7 +3312,7 @@ class StreamingProcessor { | ||
| 3378 | 3312 | this.abortController.abort(); |
| 3379 | 3313 | this.isStopped = true; |
| 3380 | 3314 | |
| 3381 | 3315 | this.hideMessageButtonsmarkUIGenStopped(this.messageId); |
| 3382 | 3316 | generatedPromptCache = ''; |
| 3383 | 3317 | unblockGeneration(); |
| 3384 | 3318 | |
| @@ -3438,7 +3372,8 @@ class StreamingProcessor { | ||
| 3438 | 3372 | if (logprobs) { |
| 3439 | 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 | 3377 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); |
| 3443 | 3378 | await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text)); |
| 3444 | 3379 | } |
| @@ -6195,20 +6130,21 @@ function extractImageFromMessage(getMessage) { | ||
| 6195 | 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 | 6136 | export function activateSendButtons() { |
| 6199 | 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 | 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 | 6145 | export function deactivateSendButtons() { |
| 6208 | - $('#send_but').addClass('displayNone'); | |
| 6209 | - $('#mes_continue').addClass('displayNone'); | |
| 6210 | - $('#mes_impersonate').addClass('displayNone'); | |
| 6211 | 6146 | showStopButton(); |
| 6147 | + document.body.dataset.generating = 'true'; | |
| 6212 | 6148 | } |
| 6213 | 6149 | |
| 6214 | 6150 | export function resetChatState() { |
| @@ -8801,7 +8737,7 @@ const swipe_right = () => { | ||
| 8801 | 8737 | // resets the timer |
| 8802 | 8738 | swipeMessage.find('.mes_timer').html(''); |
| 8803 | 8739 | swipeMessage.find('.tokenCounterDisplay').text(''); |
| 8804 | - swipeMessage.find('.mes_reasoning').html(''); | |
| 8740 | + updateReasoningUI(swipeMessage, { reset: true }); | |
| 8805 | 8741 | } else { |
| 8806 | 8742 | //console.log('showing previously generated swipe candidate, or "..."'); |
| 8807 | 8743 | //console.log('onclick right swipe calling addOneMessage'); |
| @@ -8851,7 +8787,6 @@ const swipe_right = () => { | ||
| 8851 | 8787 | if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { |
| 8852 | 8788 | console.debug('caught here 2'); |
| 8853 | 8789 | is_send_press = true; |
| 8854 | - $('.mes_buttons:last').hide(); | |
| 8855 | 8790 | await Generate('swipe'); |
| 8856 | 8791 | } else { |
| 8857 | 8792 | if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) { |
| @@ -83,7 +83,6 @@ export { | ||
| 83 | 83 | setOpenAIMessageExamples, |
| 84 | 84 | setupChatCompletionPromptManager, |
| 85 | 85 | sendOpenAIRequest, |
| 86 | - getChatCompletionModel, | |
| 87 | 86 | TokenHandler, |
| 88 | 87 | IdentifierNotFoundError, |
| 89 | 88 | Message, |
| @@ -1498,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) { | ||
| 1498 | 1497 | } |
| 1499 | 1498 | } |
| 1500 | 1499 | |
| 1501 | 1500 | export function getChatCompletionModel() { |
| 1502 | 1501 | switch (oai_settings.chat_completion_source) { |
| 1503 | 1502 | case chat_completion_sources.CLAUDE: |
| 1504 | 1503 | return oai_settings.claude_model; |
| @@ -258,6 +258,7 @@ let power_user = { | ||
| 258 | 258 | auto_parse: false, |
| 259 | 259 | add_to_prompts: false, |
| 260 | 260 | auto_expand: false, |
| 261 | + show_hidden: false, | |
| 261 | 262 | prefix: '<think>\n', |
| 262 | 263 | suffix: '\n</think>', |
| 263 | 264 | separator: '\n\n', |
| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | import { |
| 2 | 2 | moment, |
| 3 | 3 | } from '../lib.js'; |
| 4 | 4 | import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; |
| 5 | 5 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 6 | 6 | import { getCurrentLocale, t } from './i18n.js'; |
| 7 | 7 | import { MacrosParser } from './macros.js'; |
| 8 | 8 | import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js'; |
| 9 | 9 | import { Popup } from './popup.js'; |
| 10 | 10 | import { power_user } from './power-user.js'; |
| 11 | 11 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| @@ -13,7 +13,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' | ||
| 13 | 13 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | 14 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 15 | 15 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 16 | 16 | import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js'; |
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | 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 | 75 | * @paramreturns {HTMLElementboolean} elementTrue Theif elementthe tomodel updatesupports 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() { | |
| 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 | + */ | |
| 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; | |
| 81 | 380 | if (duration) { |
| 82 | 381 | const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 }); |
| 83 | 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 | 392 | element.textContent = t`Thought for some time`; |
| 393 | + data = 'unknown'; | |
| 87 | 394 | } else { |
| 88 | 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 | 408 | export class PromptReasoning { |
| 97 | 409 | static REASONING_PLACEHOLDER = '\u200B'; |
| 98 | - static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`); | |
| 99 | 410 | |
| 100 | 411 | constructor() { |
| 101 | 412 | this.counter = 0; |
| @@ -126,7 +437,7 @@ export class PromptReasoning { | ||
| 126 | 437 | return content; |
| 127 | 438 | } |
| 128 | 439 | |
| 129 | 440 | // No reasoning provided or a legacy placeholder |
| 130 | 441 | if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { |
| 131 | 442 | return content; |
| 132 | 443 | } |
| @@ -192,6 +503,15 @@ function loadReasoningSettings() { | ||
| 192 | 503 | toggleReasoningAutoExpand(); |
| 193 | 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 | 517 | function registerReasoningSlashCommands() { |
| @@ -211,7 +531,7 @@ function registerReasoningSlashCommands() { | ||
| 211 | 531 | const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1; |
| 212 | 532 | const message = chat[messageId]; |
| 213 | 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 | 658 | const textarea = document.createElement('textarea'); |
| 339 | 659 | const reasoningBlock = messageBlock.find('.mes_reasoning'); |
| 340 | 660 | textarea.classList.add('reasoning_edit_textarea'); |
| 341 | 661 | textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); |
| 342 | 662 | $(textarea).insertBefore(reasoningBlock); |
| 343 | 663 | |
| 344 | 664 | if (!CSS.supports('field-sizing', 'content')) { |
| @@ -393,10 +713,12 @@ function setReasoningEventHandlers() { | ||
| 393 | 713 | textarea.remove(); |
| 394 | 714 | |
| 395 | 715 | messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click'); |
| 716 | + | |
| 717 | + updateReasoningUI(messageBlock); | |
| 396 | 718 | }); |
| 397 | 719 | |
| 398 | 720 | $(document).on('click', '.mes_edit_add_reasoning', async function () { |
| 399 | 721 | const { message, messageId, messageBlock } = getMessageFromJquery(this); |
| 400 | 722 | if (!message?.extra) { |
| 401 | 723 | return; |
| 402 | 724 | } |
| @@ -406,8 +728,16 @@ function setReasoningEventHandlers() { | ||
| 406 | 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 | 741 | messageBlock.find('.mes_reasoning_edit').trigger('click'); |
| 412 | 742 | await saveChatConditional(); |
| 413 | 743 | }); |
| @@ -416,7 +746,7 @@ function setReasoningEventHandlers() { | ||
| 416 | 746 | e.stopPropagation(); |
| 417 | 747 | e.preventDefault(); |
| 418 | 748 | |
| 419 | 749 | const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?`,<br t`/>Visible message contents will stay intact.`); |
| 420 | 750 | |
| 421 | 751 | if (!confirm) { |
| 422 | 752 | return; |
| @@ -435,7 +765,7 @@ function setReasoningEventHandlers() { | ||
| 435 | 765 | |
| 436 | 766 | $(document).on('pointerup', '.mes_reasoning_copy', async function () { |
| 437 | 767 | const { message } = getMessageFromJquery(this); |
| 438 | 768 | const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); |
| 439 | 769 | |
| 440 | 770 | if (!reasoning) { |
| 441 | 771 | return; |
| @@ -553,7 +883,6 @@ function registerReasoningAppEvents() { | ||
| 553 | 883 | |
| 554 | 884 | export function initReasoning() { |
| 555 | 885 | loadReasoningSettings(); |
| 556 | - toggleReasoningAutoExpand(); | |
| 557 | 886 | setReasoningEventHandlers(); |
| 558 | 887 | registerReasoningSlashCommands(); |
| 559 | 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 | 2079 | export async function fetchFaFile(name) { |
| 2063 | 2080 | const style = document.createElement('style'); |
| 2064 | 2081 | style.innerHTML = await (await fetch(`/css/${name}`)).text(); |
| @@ -410,14 +410,26 @@ input[type='checkbox']:focus-visible { | ||
| 410 | 410 | } |
| 411 | 411 | |
| 412 | 412 | .mes_bias:empty, |
| 413 | -.mes_reasoning:empty, | |
| 413 | +.mes:not(.reasoning) .mes_reasoning_details, | |
| 414 | -.mes_reasoning_details:has(.mes_reasoning:empty), | |
| 415 | 414 | .mes_reasoning_details:not([open]) .mes_reasoning_actions, |
| 416 | 415 | .mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning, |
| 417 | 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 | 417 | .mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button), |
| 420 | 418 | .mes_blockmes_reasoning_details:hasnot(.edit_textarea):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 | 433 | display: none; |
| 422 | 434 | } |
| 423 | 435 | |
| @@ -4319,7 +4331,11 @@ input[type="range"]::-webkit-slider-thumb { | ||
| 4319 | 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 | 4339 | display: none; |
| 4324 | 4340 | } |
| 4325 | 4341 | |
| @@ -288,7 +288,6 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 288 | 288 | |
| 289 | 289 | const model = String(request.body.model); |
| 290 | 290 | const stream = Boolean(request.body.stream); |
| 291 | - const showThoughts = Boolean(request.body.include_reasoning); | |
| 292 | 291 | const isThinking = model.includes('thinking'); |
| 293 | 292 | |
| 294 | 293 | const generationConfig = { |
| @@ -337,12 +336,6 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 337 | 336 | body.systemInstruction = prompt.system_instruction; |
| 338 | 337 | } |
| 339 | 338 | |
| 340 | - if (isThinking && showThoughts) { | |
| 341 | - generationConfig.thinkingConfig = { | |
| 342 | - includeThoughts: true, | |
| 343 | - }; | |
| 344 | - } | |
| 345 | - | |
| 346 | 339 | return body; |
| 347 | 340 | } |
| 348 | 341 | |