mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-04-13 10:22:05 +02:00
Merge pull request #3444 from SillyTavern/hidden-reasoning-tracking
Track reasoning duration for models with hidden reasoning
This commit is contained in:
commit
25a2582d15
@ -1983,12 +1983,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="range-block" data-source="makersuite,deepseek,openrouter">
|
<div class="range-block" data-source="deepseek,openrouter">
|
||||||
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
|
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
|
||||||
<input id="openai_show_thoughts" type="checkbox" />
|
<input id="openai_show_thoughts" type="checkbox" />
|
||||||
<span>
|
<span>
|
||||||
<span data-i18n="Request model reasoning">Request model reasoning</span>
|
<span data-i18n="Request model reasoning">Request model reasoning</span>
|
||||||
<i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
|
<i class="opacity50p fa-solid fa-circle-info" title="DeepSeek Reasoner"></i>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="toggle-description justifyLeft marginBot5">
|
<div class="toggle-description justifyLeft marginBot5">
|
||||||
@ -3810,6 +3810,12 @@
|
|||||||
Auto-Expand
|
Auto-Expand
|
||||||
</small>
|
</small>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox_label flex1" for="reasoning_show_hidden" title="Show reasoning time for models with hidden reasoning." data-i18n="[title]reasoning_show_hidden">
|
||||||
|
<input id="reasoning_show_hidden" type="checkbox" />
|
||||||
|
<small data-i18n="Show Hidden">
|
||||||
|
Show Hidden
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container alignItemsBaseline">
|
<div class="flex-container alignItemsBaseline">
|
||||||
<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">
|
<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">
|
||||||
|
151
public/script.js
151
public/script.js
@ -269,7 +269,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
|
|||||||
import { initBulkEdit } from './scripts/bulk-edit.js';
|
import { initBulkEdit } from './scripts/bulk-edit.js';
|
||||||
import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
|
import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
|
||||||
import { getContext } from './scripts/st-context.js';
|
import { getContext } from './scripts/st-context.js';
|
||||||
import { extractReasoningFromData, initReasoning, PromptReasoning, removeReasoningFromString, updateReasoningTimeUI } from './scripts/reasoning.js';
|
import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
|
||||||
import { accountStorage } from './scripts/util/AccountStorage.js';
|
import { accountStorage } from './scripts/util/AccountStorage.js';
|
||||||
|
|
||||||
// API OBJECT FOR EXTERNAL WIRING
|
// API OBJECT FOR EXTERNAL WIRING
|
||||||
@ -2224,8 +2224,6 @@ function getMessageFromTemplate({
|
|||||||
isUser,
|
isUser,
|
||||||
avatarImg,
|
avatarImg,
|
||||||
bias,
|
bias,
|
||||||
reasoning,
|
|
||||||
reasoningDuration,
|
|
||||||
isSystem,
|
isSystem,
|
||||||
title,
|
title,
|
||||||
timerValue,
|
timerValue,
|
||||||
@ -2250,7 +2248,6 @@ function getMessageFromTemplate({
|
|||||||
mes.find('.avatar img').attr('src', avatarImg);
|
mes.find('.avatar img').attr('src', avatarImg);
|
||||||
mes.find('.ch_name .name_text').text(characterName);
|
mes.find('.ch_name .name_text').text(characterName);
|
||||||
mes.find('.mes_bias').html(bias);
|
mes.find('.mes_bias').html(bias);
|
||||||
mes.find('.mes_reasoning').html(reasoning);
|
|
||||||
mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
|
mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
|
||||||
mes.find('.mesIDDisplay').text(`#${mesId}`);
|
mes.find('.mesIDDisplay').text(`#${mesId}`);
|
||||||
tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
|
tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
|
||||||
@ -2258,12 +2255,7 @@ function getMessageFromTemplate({
|
|||||||
timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
|
timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
|
||||||
bookmarkLink && updateBookmarkDisplay(mes);
|
bookmarkLink && updateBookmarkDisplay(mes);
|
||||||
|
|
||||||
if (reasoning) {
|
updateReasoningUI(mes);
|
||||||
mes.addClass('reasoning');
|
|
||||||
}
|
|
||||||
if (reasoningDuration) {
|
|
||||||
updateReasoningTimeUI(mes.find('.mes_reasoning_header_title')[0], reasoningDuration, { forceEnd: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (power_user.timestamp_model_icon && extra?.api) {
|
if (power_user.timestamp_model_icon && extra?.api) {
|
||||||
insertSVGIcon(mes, extra);
|
insertSVGIcon(mes, extra);
|
||||||
@ -2285,8 +2277,9 @@ export function updateMessageBlock(messageId, message, { rerenderMessage = true
|
|||||||
const text = message?.extra?.display_text ?? message.mes;
|
const text = message?.extra?.display_text ?? message.mes;
|
||||||
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
|
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
|
||||||
}
|
}
|
||||||
messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
|
|
||||||
messageElement.toggleClass('reasoning', !!message.extra?.reasoning);
|
updateReasoningUI(messageElement);
|
||||||
|
|
||||||
addCopyToCodeBlocks(messageElement);
|
addCopyToCodeBlocks(messageElement);
|
||||||
appendMediaToMessage(message, messageElement);
|
appendMediaToMessage(message, messageElement);
|
||||||
}
|
}
|
||||||
@ -2446,7 +2439,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
|
const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
|
||||||
const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
|
|
||||||
let bookmarkLink = mes?.extra?.bookmark_link ?? '';
|
let bookmarkLink = mes?.extra?.bookmark_link ?? '';
|
||||||
|
|
||||||
let params = {
|
let params = {
|
||||||
@ -2456,8 +2448,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
|
|||||||
isUser: mes.is_user,
|
isUser: mes.is_user,
|
||||||
avatarImg: avatarImg,
|
avatarImg: avatarImg,
|
||||||
bias: bias,
|
bias: bias,
|
||||||
reasoning: reasoning,
|
|
||||||
reasoningDuration: mes.extra?.reasoning_duration,
|
|
||||||
isSystem: isSystem,
|
isSystem: isSystem,
|
||||||
title: title,
|
title: title,
|
||||||
bookmarkLink: bookmarkLink,
|
bookmarkLink: bookmarkLink,
|
||||||
@ -2465,7 +2455,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
extra: mes.extra,
|
extra: mes.extra,
|
||||||
tokenCount: mes.extra?.token_count ?? 0,
|
tokenCount: mes.extra?.token_count ?? 0,
|
||||||
...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count),
|
...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderedMessage = getMessageFromTemplate(params);
|
const renderedMessage = getMessageFromTemplate(params);
|
||||||
@ -2517,8 +2507,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
|
|||||||
const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
|
const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`);
|
||||||
swipeMessage.attr('swipeid', params.swipeId);
|
swipeMessage.attr('swipeid', params.swipeId);
|
||||||
swipeMessage.find('.mes_text').html(messageText).attr('title', title);
|
swipeMessage.find('.mes_text').html(messageText).attr('title', title);
|
||||||
swipeMessage.find('.mes_reasoning').html(reasoning);
|
|
||||||
swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
|
swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
|
||||||
|
updateReasoningUI(swipeMessage);
|
||||||
appendMediaToMessage(mes, swipeMessage);
|
appendMediaToMessage(mes, swipeMessage);
|
||||||
if (power_user.timestamp_model_icon && params.extra?.api) {
|
if (power_user.timestamp_model_icon && params.extra?.api) {
|
||||||
insertSVGIcon(swipeMessage, params.extra);
|
insertSVGIcon(swipeMessage, params.extra);
|
||||||
@ -2585,13 +2575,14 @@ export function formatCharacterAvatar(characterAvatar) {
|
|||||||
* @param {Date} gen_started Date when generation was started
|
* @param {Date} gen_started Date when generation was started
|
||||||
* @param {Date} gen_finished Date when generation was finished
|
* @param {Date} gen_finished Date when generation was finished
|
||||||
* @param {number} tokenCount Number of tokens generated (0 if not available)
|
* @param {number} tokenCount Number of tokens generated (0 if not available)
|
||||||
|
* @param {number?} [reasoningDuration=null] Reasoning duration (null if no reasoning was done)
|
||||||
* @returns {Object} Object containing the formatted timer value and title
|
* @returns {Object} Object containing the formatted timer value and title
|
||||||
* @example
|
* @example
|
||||||
* const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
|
* const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
|
||||||
* console.log(timerValue); // 1.2s
|
* console.log(timerValue); // 1.2s
|
||||||
* 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
|
* 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
|
||||||
*/
|
*/
|
||||||
function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
|
function formatGenerationTimer(gen_started, gen_finished, tokenCount, reasoningDuration = null) {
|
||||||
if (!gen_started || !gen_finished) {
|
if (!gen_started || !gen_finished) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -2605,8 +2596,9 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
|
|||||||
`Generation queued: ${start.format(dateFormat)}`,
|
`Generation queued: ${start.format(dateFormat)}`,
|
||||||
`Reply received: ${finish.format(dateFormat)}`,
|
`Reply received: ${finish.format(dateFormat)}`,
|
||||||
`Time to generate: ${seconds} seconds`,
|
`Time to generate: ${seconds} seconds`,
|
||||||
|
reasoningDuration > 0 ? `Time to think: ${reasoningDuration / 1000} seconds` : '',
|
||||||
tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
|
tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
|
||||||
].join('\n');
|
].filter(x => x).join('\n').trim();
|
||||||
|
|
||||||
if (isNaN(seconds) || seconds < 0) {
|
if (isNaN(seconds) || seconds < 0) {
|
||||||
return { timerValue: '', timerTitle };
|
return { timerValue: '', timerTitle };
|
||||||
@ -3125,15 +3117,14 @@ class StreamingProcessor {
|
|||||||
constructor(type, forceName2, timeStarted, continueMessage) {
|
constructor(type, forceName2, timeStarted, continueMessage) {
|
||||||
this.result = '';
|
this.result = '';
|
||||||
this.messageId = -1;
|
this.messageId = -1;
|
||||||
|
/** @type {HTMLElement} */
|
||||||
this.messageDom = null;
|
this.messageDom = null;
|
||||||
|
/** @type {HTMLElement} */
|
||||||
this.messageTextDom = null;
|
this.messageTextDom = null;
|
||||||
|
/** @type {HTMLElement} */
|
||||||
this.messageTimerDom = null;
|
this.messageTimerDom = null;
|
||||||
/** @type {HTMLElement} */
|
/** @type {HTMLElement} */
|
||||||
this.messageTokenCounterDom = null;
|
this.messageTokenCounterDom = null;
|
||||||
/** @type {HTMLElement} */
|
|
||||||
this.messageReasoningDom = null;
|
|
||||||
/** @type {HTMLElement} */
|
|
||||||
this.messageReasoningHeaderDom = null;
|
|
||||||
/** @type {HTMLTextAreaElement} */
|
/** @type {HTMLTextAreaElement} */
|
||||||
this.sendTextarea = document.querySelector('#send_textarea');
|
this.sendTextarea = document.querySelector('#send_textarea');
|
||||||
this.type = type;
|
this.type = type;
|
||||||
@ -3149,16 +3140,8 @@ class StreamingProcessor {
|
|||||||
/** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
|
/** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
|
||||||
this.messageLogprobs = [];
|
this.messageLogprobs = [];
|
||||||
this.toolCalls = [];
|
this.toolCalls = [];
|
||||||
this.reasoning = '';
|
// Initialize reasoning in its own handler
|
||||||
this.reasoningStartTime = null;
|
this.reasoningHandler = new ReasoningHandler(timeStarted);
|
||||||
this.reasoningEndTime = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#reasoningDuration() {
|
|
||||||
if (this.reasoningStartTime && this.reasoningEndTime) {
|
|
||||||
return (this.reasoningEndTime - this.reasoningStartTime);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#checkDomElements(messageId) {
|
#checkDomElements(messageId) {
|
||||||
@ -3167,9 +3150,8 @@ class StreamingProcessor {
|
|||||||
this.messageTextDom = this.messageDom?.querySelector('.mes_text');
|
this.messageTextDom = this.messageDom?.querySelector('.mes_text');
|
||||||
this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
|
this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
|
||||||
this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
|
this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
|
||||||
this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning');
|
|
||||||
this.messageReasoningHeaderDom = this.messageDom?.querySelector('.mes_reasoning_header_title');
|
|
||||||
}
|
}
|
||||||
|
this.reasoningHandler.updateDom(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateMessageBlockVisibility() {
|
#updateMessageBlockVisibility() {
|
||||||
@ -3179,22 +3161,12 @@ class StreamingProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessageButtons(messageId) {
|
markUIGenStarted() {
|
||||||
if (messageId == -1) {
|
deactivateSendButtons();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showStopButton();
|
|
||||||
$(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hideMessageButtons(messageId) {
|
markUIGenStopped() {
|
||||||
if (messageId == -1) {
|
activateSendButtons();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideStopButton();
|
|
||||||
$(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onStartStreaming(text) {
|
async onStartStreaming(text) {
|
||||||
@ -3203,14 +3175,12 @@ class StreamingProcessor {
|
|||||||
if (this.type == 'impersonate') {
|
if (this.type == 'impersonate') {
|
||||||
this.sendTextarea.value = '';
|
this.sendTextarea.value = '';
|
||||||
this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
|
this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
await saveReply(this.type, text, true, '', [], '');
|
await saveReply(this.type, text, true, '', [], '');
|
||||||
messageId = chat.length - 1;
|
messageId = chat.length - 1;
|
||||||
this.#checkDomElements(messageId);
|
this.#checkDomElements(messageId);
|
||||||
this.showMessageButtons(messageId);
|
this.markUIGenStarted();
|
||||||
}
|
}
|
||||||
|
|
||||||
hideSwipeButtons();
|
hideSwipeButtons();
|
||||||
scrollChatToBottom();
|
scrollChatToBottom();
|
||||||
return messageId;
|
return messageId;
|
||||||
@ -3228,11 +3198,9 @@ class StreamingProcessor {
|
|||||||
|
|
||||||
let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
|
let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
|
||||||
|
|
||||||
// Predict unbalanced asterisks / quotes during streaming
|
|
||||||
const charsToBalance = ['*', '"', '```'];
|
const charsToBalance = ['*', '"', '```'];
|
||||||
for (const char of charsToBalance) {
|
for (const char of charsToBalance) {
|
||||||
if (!isFinal && isOdd(countOccurrences(processedText, char))) {
|
if (!isFinal && isOdd(countOccurrences(processedText, char))) {
|
||||||
// Add character at the end to balance it
|
|
||||||
const separator = char.length > 1 ? '\n' : '';
|
const separator = char.length > 1 ? '\n' : '';
|
||||||
processedText = processedText.trimEnd() + separator + char;
|
processedText = processedText.trimEnd() + separator + char;
|
||||||
}
|
}
|
||||||
@ -3241,47 +3209,24 @@ class StreamingProcessor {
|
|||||||
if (isImpersonate) {
|
if (isImpersonate) {
|
||||||
this.sendTextarea.value = processedText;
|
this.sendTextarea.value = processedText;
|
||||||
this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
|
this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const mesChanged = chat[messageId]['mes'] !== processedText;
|
const mesChanged = chat[messageId]['mes'] !== processedText;
|
||||||
|
|
||||||
this.#checkDomElements(messageId);
|
this.#checkDomElements(messageId);
|
||||||
this.#updateMessageBlockVisibility();
|
this.#updateMessageBlockVisibility();
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
chat[messageId]['mes'] = processedText;
|
chat[messageId]['mes'] = processedText;
|
||||||
chat[messageId]['gen_started'] = this.timeStarted;
|
chat[messageId]['gen_started'] = this.timeStarted;
|
||||||
chat[messageId]['gen_finished'] = currentTime;
|
chat[messageId]['gen_finished'] = currentTime;
|
||||||
|
|
||||||
if (!chat[messageId]['extra']) {
|
if (!chat[messageId]['extra']) {
|
||||||
chat[messageId]['extra'] = {};
|
chat[messageId]['extra'] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.reasoning) {
|
// Update reasoning
|
||||||
const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
|
await this.reasoningHandler.process(messageId, mesChanged);
|
||||||
const reasoningChanged = chat[messageId]['extra']['reasoning'] !== reasoning;
|
|
||||||
chat[messageId]['extra']['reasoning'] = reasoning;
|
|
||||||
|
|
||||||
if (reasoningChanged && this.reasoningStartTime === null) {
|
// Token count update.
|
||||||
this.reasoningStartTime = Date.now();
|
const tokenCountText = this.reasoningHandler.reasoning + processedText;
|
||||||
}
|
|
||||||
if (!reasoningChanged && mesChanged && this.reasoningStartTime !== null && this.reasoningEndTime === null) {
|
|
||||||
this.reasoningEndTime = Date.now();
|
|
||||||
}
|
|
||||||
await this.#updateReasoningTime(messageId);
|
|
||||||
|
|
||||||
if (this.messageReasoningDom instanceof HTMLElement) {
|
|
||||||
const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
|
|
||||||
this.messageReasoningDom.innerHTML = formattedReasoning;
|
|
||||||
}
|
|
||||||
if (this.messageDom instanceof HTMLElement) {
|
|
||||||
this.messageDom.classList.add('reasoning');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't waste time calculating token count for streaming
|
|
||||||
const tokenCountText = (this.reasoning || '') + processedText;
|
|
||||||
const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
|
const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;
|
||||||
|
|
||||||
if (currentTokenCount) {
|
if (currentTokenCount) {
|
||||||
chat[messageId]['extra']['token_count'] = currentTokenCount;
|
chat[messageId]['extra']['token_count'] = currentTokenCount;
|
||||||
if (this.messageTokenCounterDom instanceof HTMLElement) {
|
if (this.messageTokenCounterDom instanceof HTMLElement) {
|
||||||
@ -3307,7 +3252,7 @@ class StreamingProcessor {
|
|||||||
this.messageTextDom.innerHTML = formattedText;
|
this.messageTextDom.innerHTML = formattedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
|
const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration());
|
||||||
if (this.messageTimerDom instanceof HTMLElement) {
|
if (this.messageTimerDom instanceof HTMLElement) {
|
||||||
this.messageTimerDom.textContent = timePassed.timerValue;
|
this.messageTimerDom.textContent = timePassed.timerValue;
|
||||||
this.messageTimerDom.title = timePassed.timerTitle;
|
this.messageTimerDom.title = timePassed.timerTitle;
|
||||||
@ -3321,23 +3266,12 @@ class StreamingProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #updateReasoningTime(messageId, { forceEnd = false } = {}) {
|
|
||||||
const duration = this.#reasoningDuration();
|
|
||||||
chat[messageId]['extra']['reasoning_duration'] = duration;
|
|
||||||
updateReasoningTimeUI(this.messageReasoningHeaderDom, duration, { forceEnd: forceEnd });
|
|
||||||
await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onFinishStreaming(messageId, text) {
|
async onFinishStreaming(messageId, text) {
|
||||||
this.hideMessageButtons(this.messageId);
|
this.markUIGenStopped();
|
||||||
await this.onProgressStreaming(messageId, text, true);
|
await this.onProgressStreaming(messageId, text, true);
|
||||||
addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
|
addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
|
||||||
|
|
||||||
// Ensure reasoning finish time is recorded if not already
|
await this.reasoningHandler.finish(messageId);
|
||||||
if (this.reasoningStartTime !== null && this.reasoningEndTime === null) {
|
|
||||||
this.reasoningEndTime = Date.now();
|
|
||||||
await this.#updateReasoningTime(messageId, { forceEnd: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(this.swipes) && this.swipes.length > 0) {
|
if (Array.isArray(this.swipes) && this.swipes.length > 0) {
|
||||||
const message = chat[messageId];
|
const message = chat[messageId];
|
||||||
@ -3378,7 +3312,7 @@ class StreamingProcessor {
|
|||||||
this.abortController.abort();
|
this.abortController.abort();
|
||||||
this.isStopped = true;
|
this.isStopped = true;
|
||||||
|
|
||||||
this.hideMessageButtons(this.messageId);
|
this.markUIGenStopped();
|
||||||
generatedPromptCache = '';
|
generatedPromptCache = '';
|
||||||
unblockGeneration();
|
unblockGeneration();
|
||||||
|
|
||||||
@ -3438,7 +3372,8 @@ class StreamingProcessor {
|
|||||||
if (logprobs) {
|
if (logprobs) {
|
||||||
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
|
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
|
||||||
}
|
}
|
||||||
this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
|
// Get the updated reasoning string into the handler
|
||||||
|
this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');
|
||||||
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
|
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
|
||||||
await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
|
await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
|
||||||
}
|
}
|
||||||
@ -6195,20 +6130,21 @@ function extractImageFromMessage(getMessage) {
|
|||||||
return { getMessage, image, title };
|
return { getMessage, image, title };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function mainly used to switch 'generating' state - setting it to false and activating the buttons again
|
||||||
|
*/
|
||||||
export function activateSendButtons() {
|
export function activateSendButtons() {
|
||||||
is_send_press = false;
|
is_send_press = false;
|
||||||
$('#send_but').removeClass('displayNone');
|
|
||||||
$('#mes_continue').removeClass('displayNone');
|
|
||||||
$('#mes_impersonate').removeClass('displayNone');
|
|
||||||
$('.mes_buttons:last').show();
|
|
||||||
hideStopButton();
|
hideStopButton();
|
||||||
|
delete document.body.dataset.generating;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function mainly used to switch 'generating' state - setting it to true and deactivating the buttons
|
||||||
|
*/
|
||||||
export function deactivateSendButtons() {
|
export function deactivateSendButtons() {
|
||||||
$('#send_but').addClass('displayNone');
|
|
||||||
$('#mes_continue').addClass('displayNone');
|
|
||||||
$('#mes_impersonate').addClass('displayNone');
|
|
||||||
showStopButton();
|
showStopButton();
|
||||||
|
document.body.dataset.generating = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetChatState() {
|
export function resetChatState() {
|
||||||
@ -8801,7 +8737,7 @@ const swipe_right = () => {
|
|||||||
// resets the timer
|
// resets the timer
|
||||||
swipeMessage.find('.mes_timer').html('');
|
swipeMessage.find('.mes_timer').html('');
|
||||||
swipeMessage.find('.tokenCounterDisplay').text('');
|
swipeMessage.find('.tokenCounterDisplay').text('');
|
||||||
swipeMessage.find('.mes_reasoning').html('');
|
updateReasoningUI(swipeMessage, { reset: true });
|
||||||
} else {
|
} else {
|
||||||
//console.log('showing previously generated swipe candidate, or "..."');
|
//console.log('showing previously generated swipe candidate, or "..."');
|
||||||
//console.log('onclick right swipe calling addOneMessage');
|
//console.log('onclick right swipe calling addOneMessage');
|
||||||
@ -8851,7 +8787,6 @@ const swipe_right = () => {
|
|||||||
if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
|
if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
|
||||||
console.debug('caught here 2');
|
console.debug('caught here 2');
|
||||||
is_send_press = true;
|
is_send_press = true;
|
||||||
$('.mes_buttons:last').hide();
|
|
||||||
await Generate('swipe');
|
await Generate('swipe');
|
||||||
} else {
|
} else {
|
||||||
if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
|
if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
|
||||||
|
@ -83,7 +83,6 @@ export {
|
|||||||
setOpenAIMessageExamples,
|
setOpenAIMessageExamples,
|
||||||
setupChatCompletionPromptManager,
|
setupChatCompletionPromptManager,
|
||||||
sendOpenAIRequest,
|
sendOpenAIRequest,
|
||||||
getChatCompletionModel,
|
|
||||||
TokenHandler,
|
TokenHandler,
|
||||||
IdentifierNotFoundError,
|
IdentifierNotFoundError,
|
||||||
Message,
|
Message,
|
||||||
@ -1498,7 +1497,7 @@ async function sendWindowAIRequest(messages, signal, stream) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChatCompletionModel() {
|
export function getChatCompletionModel() {
|
||||||
switch (oai_settings.chat_completion_source) {
|
switch (oai_settings.chat_completion_source) {
|
||||||
case chat_completion_sources.CLAUDE:
|
case chat_completion_sources.CLAUDE:
|
||||||
return oai_settings.claude_model;
|
return oai_settings.claude_model;
|
||||||
|
@ -258,6 +258,7 @@ let power_user = {
|
|||||||
auto_parse: false,
|
auto_parse: false,
|
||||||
add_to_prompts: false,
|
add_to_prompts: false,
|
||||||
auto_expand: false,
|
auto_expand: false,
|
||||||
|
show_hidden: false,
|
||||||
prefix: '<think>\n',
|
prefix: '<think>\n',
|
||||||
suffix: '\n</think>',
|
suffix: '\n</think>',
|
||||||
separator: '\n\n',
|
separator: '\n\n',
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
moment,
|
moment,
|
||||||
} from '../lib.js';
|
} from '../lib.js';
|
||||||
import { chat, closeMessageEditor, event_types, eventSource, main_api, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
|
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
|
||||||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||||||
import { getCurrentLocale, t } from './i18n.js';
|
import { getCurrentLocale, t } from './i18n.js';
|
||||||
import { MacrosParser } from './macros.js';
|
import { MacrosParser } from './macros.js';
|
||||||
import { chat_completion_sources, oai_settings } from './openai.js';
|
import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
|
||||||
import { Popup } from './popup.js';
|
import { Popup } from './popup.js';
|
||||||
import { power_user } from './power-user.js';
|
import { power_user } from './power-user.js';
|
||||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||||
@ -13,7 +13,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
|
|||||||
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||||
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
||||||
import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
|
import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty } from './utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a message from a jQuery element.
|
* Gets a message from a jQuery element.
|
||||||
@ -71,21 +71,333 @@ export function extractReasoningFromData(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the Reasoning controls
|
* Check if the model supports reasoning, but does not send back the reasoning
|
||||||
* @param {HTMLElement} element The element to update
|
* @returns {boolean} True if the model supports reasoning
|
||||||
* @param {number?} duration The duration of the reasoning in milliseconds
|
|
||||||
* @param {object} [options={}] Options for the function
|
|
||||||
* @param {boolean} [options.forceEnd=false] If true, there will be no "Thinking..." when no duration exists
|
|
||||||
*/
|
*/
|
||||||
export function updateReasoningTimeUI(element, duration, { forceEnd = false } = {}) {
|
export function isHiddenReasoningModel() {
|
||||||
if (duration) {
|
if (main_api !== 'openai') {
|
||||||
const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
|
return false;
|
||||||
const secondsStr = moment.duration(duration).asSeconds();
|
}
|
||||||
element.innerHTML = t`Thought for <span title="${secondsStr} seconds">${durationStr}</span>`;
|
|
||||||
} else if (forceEnd) {
|
/** @typedef {{ (currentModel: string, supportedModel: string): boolean }} MatchingFunc */
|
||||||
element.textContent = t`Thought for some time`;
|
/** @type {Record.<string, MatchingFunc>} */
|
||||||
} else {
|
const FUNCS = {
|
||||||
element.textContent = t`Thinking...`;
|
equals: (currentModel, supportedModel) => currentModel === supportedModel,
|
||||||
|
startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {{ name: string; func: MatchingFunc; }[]} */
|
||||||
|
const hiddenReasoningModels = [
|
||||||
|
{ name: 'o1', func: FUNCS.startsWith },
|
||||||
|
{ name: 'o3', func: FUNCS.startsWith },
|
||||||
|
{ name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith },
|
||||||
|
{ name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
|
||||||
|
];
|
||||||
|
|
||||||
|
const model = getChatCompletionModel();
|
||||||
|
|
||||||
|
const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
|
||||||
|
return isHidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the Reasoning UI for a specific message
|
||||||
|
* @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element
|
||||||
|
* @param {Object} [options={}] - Optional arguments
|
||||||
|
* @param {boolean} [options.reset=false] - Whether to reset state, and not take the current mess properties (for example when swiping)
|
||||||
|
*/
|
||||||
|
export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) {
|
||||||
|
const handler = new ReasoningHandler();
|
||||||
|
handler.initHandleMessage(messageIdOrElement, { reset });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for representing the state of reasoning
|
||||||
|
* @enum {string}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
export const ReasoningState = {
|
||||||
|
None: 'none',
|
||||||
|
Thinking: 'thinking',
|
||||||
|
Done: 'done',
|
||||||
|
Hidden: 'hidden',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles reasoning-specific logic and DOM updates for messages.
|
||||||
|
* This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
|
||||||
|
*/
|
||||||
|
export class ReasoningHandler {
|
||||||
|
#isHiddenReasoningModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Date?} [timeStarted=null] - When the generation started
|
||||||
|
*/
|
||||||
|
constructor(timeStarted = null) {
|
||||||
|
/** @type {ReasoningState} The current state of the reasoning process */
|
||||||
|
this.state = ReasoningState.None;
|
||||||
|
/** @type {string} The reasoning output */
|
||||||
|
this.reasoning = '';
|
||||||
|
/** @type {Date} When the reasoning started */
|
||||||
|
this.startTime = null;
|
||||||
|
/** @type {Date} When the reasoning ended */
|
||||||
|
this.endTime = null;
|
||||||
|
|
||||||
|
/** @type {Date} Initial starting time of the generation */
|
||||||
|
this.initialTime = timeStarted ?? new Date();
|
||||||
|
|
||||||
|
/** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
|
||||||
|
this.#isHiddenReasoningModel = isHiddenReasoningModel();
|
||||||
|
|
||||||
|
// Cached DOM elements for reasoning
|
||||||
|
/** @type {HTMLElement} Main message DOM element `.mes` */
|
||||||
|
this.messageDom = null;
|
||||||
|
/** @type {HTMLElement} Reasoning details DOM element `.mes_reasoning_details` */
|
||||||
|
this.messageReasoningDetailsDom = null;
|
||||||
|
/** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
|
||||||
|
this.messageReasoningContentDom = null;
|
||||||
|
/** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
|
||||||
|
this.messageReasoningHeaderDom = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the reasoning handler for a specific message.
|
||||||
|
*
|
||||||
|
* Can be used to update the DOM elements or read other reasoning states.
|
||||||
|
* It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
|
||||||
|
* The state will always be either done/hidden or none.
|
||||||
|
*
|
||||||
|
* @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
|
||||||
|
* @param {Object} [options={}] - Optional arguments
|
||||||
|
* @param {boolean} [options.reset=false] - Whether to reset state of the handler, and not take the current mess properties (for example when swiping)
|
||||||
|
*/
|
||||||
|
initHandleMessage(messageIdOrElement, { reset = false } = {}) {
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
const messageElement = typeof messageIdOrElement === 'number'
|
||||||
|
? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
|
||||||
|
: messageIdOrElement instanceof HTMLElement
|
||||||
|
? messageIdOrElement
|
||||||
|
: $(messageIdOrElement)[0];
|
||||||
|
const messageId = Number(messageElement.getAttribute('mesid'));
|
||||||
|
|
||||||
|
if (isNaN(messageId)) return;
|
||||||
|
|
||||||
|
const extra = chat[messageId].extra;
|
||||||
|
|
||||||
|
if (extra.reasoning) {
|
||||||
|
this.state = ReasoningState.Done;
|
||||||
|
} else if (extra.reasoning_duration) {
|
||||||
|
this.state = ReasoningState.Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reasoning = extra?.reasoning ?? '';
|
||||||
|
|
||||||
|
if (this.state !== ReasoningState.None) {
|
||||||
|
this.initialTime = new Date(chat[messageId].gen_started);
|
||||||
|
this.startTime = this.initialTime;
|
||||||
|
this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefill main dom element, as message might not have been rendered yet
|
||||||
|
this.messageDom = messageElement;
|
||||||
|
|
||||||
|
// Make sure reset correctly clears all relevant states
|
||||||
|
if (reset) {
|
||||||
|
this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
|
||||||
|
this.reasoning = '';
|
||||||
|
this.initialTime = new Date();
|
||||||
|
this.startTime = null;
|
||||||
|
this.endTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateDom(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the duration of the reasoning in milliseconds.
|
||||||
|
*
|
||||||
|
* @returns {number?} The duration in milliseconds, or null if the start or end time is not set
|
||||||
|
*/
|
||||||
|
getDuration() {
|
||||||
|
if (this.startTime && this.endTime) {
|
||||||
|
return this.endTime.getTime() - this.startTime.getTime();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the reasoning text/string for a message.
|
||||||
|
*
|
||||||
|
* @param {number} messageId - The ID of the message to update
|
||||||
|
* @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning
|
||||||
|
* @param {Object} [options={}] - Optional arguments
|
||||||
|
* @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
|
||||||
|
* @returns {boolean} - Returns true if the reasoning was changed, otherwise false
|
||||||
|
*/
|
||||||
|
updateReasoning(messageId, reasoning = null, { persist = false } = {}) {
|
||||||
|
reasoning = reasoning ?? this.reasoning;
|
||||||
|
reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning;
|
||||||
|
|
||||||
|
// Ensure the chat extra exists
|
||||||
|
if (!chat[messageId].extra) {
|
||||||
|
chat[messageId].extra = {};
|
||||||
|
}
|
||||||
|
const extra = chat[messageId].extra;
|
||||||
|
|
||||||
|
const reasoningChanged = extra.reasoning !== reasoning;
|
||||||
|
this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
|
||||||
|
|
||||||
|
if (persist) {
|
||||||
|
// Build and save the reasoning data to message extras
|
||||||
|
extra.reasoning = this.reasoning;
|
||||||
|
extra.reasoning_duration = this.getDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
return reasoningChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles processing of reasoning for a message.
|
||||||
|
*
|
||||||
|
* This is usually called by the message processor when a message is changed.
|
||||||
|
*
|
||||||
|
* @param {number} messageId - The ID of the message to process
|
||||||
|
* @param {boolean} mesChanged - Whether the message has changed
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async process(messageId, mesChanged) {
|
||||||
|
if (!this.reasoning && !this.#isHiddenReasoningModel) return;
|
||||||
|
|
||||||
|
// Ensure reasoning string is updated and regexes are applied correctly
|
||||||
|
const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
|
||||||
|
|
||||||
|
if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
|
||||||
|
this.state = ReasoningState.Thinking;
|
||||||
|
this.startTime = this.initialTime;
|
||||||
|
}
|
||||||
|
if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
|
||||||
|
this.endTime = new Date();
|
||||||
|
await this.finish(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completes the reasoning process for a message.
|
||||||
|
*
|
||||||
|
* Records the finish time if it was not set during streaming and updates the reasoning state.
|
||||||
|
* Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
|
||||||
|
*
|
||||||
|
* @param {number} messageId - The ID of the message to complete reasoning for
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async finish(messageId) {
|
||||||
|
if (this.state === ReasoningState.None) return;
|
||||||
|
|
||||||
|
// Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
|
||||||
|
if (this.startTime !== null && this.endTime === null) {
|
||||||
|
this.endTime = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state === ReasoningState.Thinking) {
|
||||||
|
this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
|
||||||
|
this.updateReasoning(messageId, null, { persist: true });
|
||||||
|
await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateDom(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the reasoning UI elements for a message.
|
||||||
|
*
|
||||||
|
* Toggles the CSS class, updates states, reasoning message, and duration.
|
||||||
|
*
|
||||||
|
* @param {number} messageId - The ID of the message to update
|
||||||
|
*/
|
||||||
|
updateDom(messageId) {
|
||||||
|
this.#checkDomElements(messageId);
|
||||||
|
|
||||||
|
// Main CSS class to show this message includes reasoning
|
||||||
|
this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
|
||||||
|
|
||||||
|
// Update states to the relevant DOM elements
|
||||||
|
setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
|
||||||
|
setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
|
||||||
|
|
||||||
|
// Update the reasoning message
|
||||||
|
const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
|
||||||
|
const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
|
||||||
|
this.messageReasoningContentDom.innerHTML = displayReasoning;
|
||||||
|
|
||||||
|
// Update tooltip for hidden reasoning edit
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
const button = this.messageDom.querySelector('.mes_edit_add_reasoning');
|
||||||
|
button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`;
|
||||||
|
|
||||||
|
// Update the reasoning duration in the UI
|
||||||
|
this.#updateReasoningTimeUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and caches reasoning-related DOM elements for the given message.
|
||||||
|
*
|
||||||
|
* @param {number} messageId - The ID of the message to cache the DOM elements for
|
||||||
|
*/
|
||||||
|
#checkDomElements(messageId) {
|
||||||
|
// Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
|
||||||
|
if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
|
||||||
|
this.messageDom = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the DOM elements once
|
||||||
|
if (this.messageDom === null) {
|
||||||
|
this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
|
||||||
|
if (this.messageDom === null) throw new Error('message dom does not exist');
|
||||||
|
}
|
||||||
|
if (this.messageReasoningDetailsDom === null) {
|
||||||
|
this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
|
||||||
|
}
|
||||||
|
if (this.messageReasoningContentDom === null) {
|
||||||
|
this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
|
||||||
|
}
|
||||||
|
if (this.messageReasoningHeaderDom === null) {
|
||||||
|
this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the reasoning time display in the UI.
|
||||||
|
*
|
||||||
|
* Shows the duration in a human-readable format with a tooltip for exact seconds.
|
||||||
|
* Displays "Thinking..." if still processing, or a generic message otherwise.
|
||||||
|
*/
|
||||||
|
#updateReasoningTimeUI() {
|
||||||
|
const element = this.messageReasoningHeaderDom;
|
||||||
|
const duration = this.getDuration();
|
||||||
|
let data = null;
|
||||||
|
if (duration) {
|
||||||
|
const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
|
||||||
|
const secondsStr = moment.duration(duration).asSeconds();
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.title = t`${secondsStr} seconds`;
|
||||||
|
span.textContent = durationStr;
|
||||||
|
|
||||||
|
element.textContent = t`Thought for `;
|
||||||
|
element.appendChild(span);
|
||||||
|
data = String(secondsStr);
|
||||||
|
} else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
|
||||||
|
element.textContent = t`Thought for some time`;
|
||||||
|
data = 'unknown';
|
||||||
|
} else {
|
||||||
|
element.textContent = t`Thinking...`;
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
|
||||||
|
setDatasetProperty(element, 'duration', data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +407,6 @@ export function updateReasoningTimeUI(element, duration, { forceEnd = false } =
|
|||||||
*/
|
*/
|
||||||
export class PromptReasoning {
|
export class PromptReasoning {
|
||||||
static REASONING_PLACEHOLDER = '\u200B';
|
static REASONING_PLACEHOLDER = '\u200B';
|
||||||
static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`);
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.counter = 0;
|
this.counter = 0;
|
||||||
@ -126,7 +437,7 @@ export class PromptReasoning {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No reasoning provided or a placeholder
|
// No reasoning provided or a legacy placeholder
|
||||||
if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
|
if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@ -192,6 +503,15 @@ function loadReasoningSettings() {
|
|||||||
toggleReasoningAutoExpand();
|
toggleReasoningAutoExpand();
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
toggleReasoningAutoExpand();
|
||||||
|
|
||||||
|
$('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
|
||||||
|
$('#reasoning_show_hidden').on('change', function () {
|
||||||
|
power_user.reasoning.show_hidden = !!$(this).prop('checked');
|
||||||
|
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerReasoningSlashCommands() {
|
function registerReasoningSlashCommands() {
|
||||||
@ -211,7 +531,7 @@ function registerReasoningSlashCommands() {
|
|||||||
const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;
|
const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1;
|
||||||
const message = chat[messageId];
|
const message = chat[messageId];
|
||||||
const reasoning = String(message?.extra?.reasoning ?? '');
|
const reasoning = String(message?.extra?.reasoning ?? '');
|
||||||
return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
|
return reasoning;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -338,7 +658,7 @@ function setReasoningEventHandlers() {
|
|||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
const reasoningBlock = messageBlock.find('.mes_reasoning');
|
const reasoningBlock = messageBlock.find('.mes_reasoning');
|
||||||
textarea.classList.add('reasoning_edit_textarea');
|
textarea.classList.add('reasoning_edit_textarea');
|
||||||
textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
|
textarea.value = reasoning;
|
||||||
$(textarea).insertBefore(reasoningBlock);
|
$(textarea).insertBefore(reasoningBlock);
|
||||||
|
|
||||||
if (!CSS.supports('field-sizing', 'content')) {
|
if (!CSS.supports('field-sizing', 'content')) {
|
||||||
@ -393,10 +713,12 @@ function setReasoningEventHandlers() {
|
|||||||
textarea.remove();
|
textarea.remove();
|
||||||
|
|
||||||
messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
|
messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click');
|
||||||
|
|
||||||
|
updateReasoningUI(messageBlock);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on('click', '.mes_edit_add_reasoning', async function () {
|
$(document).on('click', '.mes_edit_add_reasoning', async function () {
|
||||||
const { message, messageId, messageBlock } = getMessageFromJquery(this);
|
const { message, messageBlock } = getMessageFromJquery(this);
|
||||||
if (!message?.extra) {
|
if (!message?.extra) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -406,8 +728,16 @@ function setReasoningEventHandlers() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
|
messageBlock.addClass('reasoning');
|
||||||
updateMessageBlock(messageId, message, { rerenderMessage: false });
|
|
||||||
|
// To make hidden reasoning blocks editable, we just set them to "Done" here already.
|
||||||
|
// They will be done on save anyway - and on cancel the reasoning block gets rerendered too.
|
||||||
|
if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) {
|
||||||
|
messageBlock.attr('data-reasoning-state', ReasoningState.Done);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the reasoning area so we can actually edit it
|
||||||
|
messageBlock.find('.mes_reasoning_details').attr('open', '');
|
||||||
messageBlock.find('.mes_reasoning_edit').trigger('click');
|
messageBlock.find('.mes_reasoning_edit').trigger('click');
|
||||||
await saveChatConditional();
|
await saveChatConditional();
|
||||||
});
|
});
|
||||||
@ -416,7 +746,7 @@ function setReasoningEventHandlers() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
|
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.`);
|
||||||
|
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
return;
|
return;
|
||||||
@ -435,7 +765,7 @@ function setReasoningEventHandlers() {
|
|||||||
|
|
||||||
$(document).on('pointerup', '.mes_reasoning_copy', async function () {
|
$(document).on('pointerup', '.mes_reasoning_copy', async function () {
|
||||||
const { message } = getMessageFromJquery(this);
|
const { message } = getMessageFromJquery(this);
|
||||||
const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, '');
|
const reasoning = String(message?.extra?.reasoning ?? '');
|
||||||
|
|
||||||
if (!reasoning) {
|
if (!reasoning) {
|
||||||
return;
|
return;
|
||||||
@ -553,7 +883,6 @@ function registerReasoningAppEvents() {
|
|||||||
|
|
||||||
export function initReasoning() {
|
export function initReasoning() {
|
||||||
loadReasoningSettings();
|
loadReasoningSettings();
|
||||||
toggleReasoningAutoExpand();
|
|
||||||
setReasoningEventHandlers();
|
setReasoningEventHandlers();
|
||||||
registerReasoningSlashCommands();
|
registerReasoningSlashCommands();
|
||||||
registerReasoningMacros();
|
registerReasoningMacros();
|
||||||
|
@ -2059,6 +2059,23 @@ export function toggleDrawer(drawer, expand = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets or removes a dataset property on an HTMLElement
|
||||||
|
*
|
||||||
|
* Utility function to make it easier to reset dataset properties on null, without them being "null" as value.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element - The element to modify
|
||||||
|
* @param {string} name - The name of the dataset property
|
||||||
|
* @param {string|null} value - The value to set - If null, the dataset property will be removed
|
||||||
|
*/
|
||||||
|
export function setDatasetProperty(element, name, value) {
|
||||||
|
if (value === null) {
|
||||||
|
delete element.dataset[name];
|
||||||
|
} else {
|
||||||
|
element.dataset[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchFaFile(name) {
|
export async function fetchFaFile(name) {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.innerHTML = await (await fetch(`/css/${name}`)).text();
|
style.innerHTML = await (await fetch(`/css/${name}`)).text();
|
||||||
|
@ -410,14 +410,26 @@ input[type='checkbox']:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mes_bias:empty,
|
.mes_bias:empty,
|
||||||
.mes_reasoning:empty,
|
.mes:not(.reasoning) .mes_reasoning_details,
|
||||||
.mes_reasoning_details:has(.mes_reasoning:empty),
|
|
||||||
.mes_reasoning_details:not([open]) .mes_reasoning_actions,
|
.mes_reasoning_details:not([open]) .mes_reasoning_actions,
|
||||||
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
|
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning,
|
||||||
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
|
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_header,
|
||||||
.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
|
|
||||||
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
|
.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.edit_button),
|
||||||
.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions {
|
.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .edit_button,
|
||||||
|
.mes_block:has(.edit_textarea):has(.reasoning_edit_textarea) .mes_reasoning_actions,
|
||||||
|
.mes.reasoning:not([data-reasoning-state="hidden"]) .mes_edit_add_reasoning,
|
||||||
|
.mes:has(.mes_reasoning:empty) .mes_reasoning_arrow,
|
||||||
|
.mes:has(.mes_reasoning:empty) .mes_reasoning,
|
||||||
|
.mes:has(.mes_reasoning:empty) .mes_reasoning_copy {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mes[data-reasoning-state="hidden"] .mes_edit_add_reasoning {
|
||||||
|
background-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 33%, var(--SmartThemeBlurTintColor) 66%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If hidden reasoning should not be shown, we hide all blocks that don't have content */
|
||||||
|
#chat:not([data-show-hidden-reasoning="true"]) .mes:has(.mes_reasoning:empty) .mes_reasoning_details {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4319,7 +4331,11 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
field-sizing: content;
|
field-sizing: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mes.reasoning .mes_edit_add_reasoning {
|
body[data-generating="true"] #send_but,
|
||||||
|
body[data-generating="true"] #mes_continue,
|
||||||
|
body[data-generating="true"] #mes_impersonate,
|
||||||
|
body[data-generating="true"] #chat .last_mes .mes_buttons,
|
||||||
|
body[data-generating="true"] #chat .last_mes .mes_reasoning_actions {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,7 +288,6 @@ async function sendMakerSuiteRequest(request, response) {
|
|||||||
|
|
||||||
const model = String(request.body.model);
|
const model = String(request.body.model);
|
||||||
const stream = Boolean(request.body.stream);
|
const stream = Boolean(request.body.stream);
|
||||||
const showThoughts = Boolean(request.body.include_reasoning);
|
|
||||||
const isThinking = model.includes('thinking');
|
const isThinking = model.includes('thinking');
|
||||||
|
|
||||||
const generationConfig = {
|
const generationConfig = {
|
||||||
@ -337,12 +336,6 @@ async function sendMakerSuiteRequest(request, response) {
|
|||||||
body.systemInstruction = prompt.system_instruction;
|
body.systemInstruction = prompt.system_instruction;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isThinking && showThoughts) {
|
|
||||||
generationConfig.thinkingConfig = {
|
|
||||||
includeThoughts: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user