From afae8d02be1e837b1dc5abc013fa011ab4c3d067 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Jan 2025 02:52:52 +0200 Subject: [PATCH 01/83] The THONKening --- public/index.html | 1 + public/script.js | 111 +++++++++++++++------ public/scripts/constants.js | 5 + public/scripts/kai-settings.js | 2 +- public/scripts/nai-settings.js | 2 +- public/scripts/openai.js | 19 ++-- public/scripts/textgen-settings.js | 3 +- public/style.css | 28 +++++- src/constants.js | 5 + src/endpoints/backends/chat-completions.js | 3 +- 10 files changed, 130 insertions(+), 49 deletions(-) diff --git a/public/index.html b/public/index.html index 3984afa5e..3fe2162fb 100644 --- a/public/index.html +++ b/public/index.html @@ -6229,6 +6229,7 @@ +
diff --git a/public/script.js b/public/script.js index 3ba3aacbc..b641e161c 100644 --- a/public/script.js +++ b/public/script.js @@ -170,7 +170,7 @@ import { isElementInViewport, copyText, } from './scripts/utils.js'; -import { debounce_timeout } from './scripts/constants.js'; +import { debounce_timeout, THINK_BREAK } from './scripts/constants.js'; import { doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js'; import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution } from './scripts/slash-commands.js'; @@ -2199,6 +2199,7 @@ function getMessageFromTemplate({ isUser, avatarImg, bias, + reasoning, isSystem, title, timerValue, @@ -2223,6 +2224,7 @@ function getMessageFromTemplate({ mes.find('.avatar img').attr('src', avatarImg); mes.find('.ch_name .name_text').text(characterName); 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('.mesIDDisplay').text(`#${mesId}`); tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`); @@ -2241,6 +2243,7 @@ export function updateMessageBlock(messageId, message) { const messageElement = $(`#chat [mesid="${messageId}"]`); const text = message?.extra?.display_text ?? message.mes; messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId)); + messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1)); addCopyToCodeBlocks(messageElement); appendMediaToMessage(message, messageElement); } @@ -2399,6 +2402,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll sanitizerOverrides, ); const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1); + const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1); let bookmarkLink = mes?.extra?.bookmark_link ?? ''; let params = { @@ -2408,6 +2412,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll isUser: mes.is_user, avatarImg: avatarImg, bias: bias, + reasoning: reasoning, isSystem: isSystem, title: title, bookmarkLink: bookmarkLink, @@ -2467,6 +2472,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`); swipeMessage.attr('swipeid', params.swipeId); 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}`); appendMediaToMessage(mes, swipeMessage); if (power_user.timestamp_model_icon && params.extra?.api) { @@ -3077,6 +3083,7 @@ class StreamingProcessor { this.messageTextDom = null; this.messageTimerDom = null; this.messageTokenCounterDom = null; + this.messageReasoningDom = null; /** @type {HTMLTextAreaElement} */ this.sendTextarea = document.querySelector('#send_textarea'); this.type = type; @@ -3092,6 +3099,7 @@ class StreamingProcessor { /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */ this.messageLogprobs = []; this.toolCalls = []; + this.reasoning = ''; } #checkDomElements(messageId) { @@ -3100,6 +3108,7 @@ class StreamingProcessor { this.messageTextDom = this.messageDom?.querySelector('.mes_text'); this.messageTimerDom = this.messageDom?.querySelector('.mes_timer'); this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay'); + this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning'); } } @@ -3184,11 +3193,17 @@ class StreamingProcessor { chat[messageId]['gen_started'] = this.timeStarted; chat[messageId]['gen_finished'] = currentTime; - if (currentTokenCount) { - if (!chat[messageId]['extra']) { - chat[messageId]['extra'] = {}; - } + if (!chat[messageId]['extra']) { + chat[messageId]['extra'] = {}; + } + if (this.reasoning && this.messageReasoningDom instanceof HTMLElement) { + chat[messageId]['extra']['reasoning'] = this.reasoning; + const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1); + this.messageReasoningDom.innerHTML = formattedReasoning; + } + + if (currentTokenCount) { chat[messageId]['extra']['token_count'] = currentTokenCount; if (this.messageTokenCounterDom instanceof HTMLElement) { this.messageTokenCounterDom.textContent = `${currentTokenCount}t`; @@ -3320,7 +3335,7 @@ class StreamingProcessor { } /** - * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>} + * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>} */ *nullStreamingGeneration() { throw new Error('Generation function for streaming is not hooked up'); @@ -3342,7 +3357,7 @@ class StreamingProcessor { try { const sw = new Stopwatch(1000 / power_user.streaming_fps); const timestamps = []; - for await (const { text, swipes, logprobs, toolCalls } of this.generator()) { + for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) { timestamps.push(Date.now()); if (this.isStopped) { return; @@ -3354,6 +3369,7 @@ class StreamingProcessor { if (logprobs) { this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); } + this.reasoning = state?.reasoning ?? ''; await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text)); } @@ -4741,6 +4757,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro //const getData = await response.json(); let getMessage = extractMessageFromData(data); let title = extractTitleFromData(data); + let reasoning = extractReasoningFromData(data); kobold_horde_model = title; const swipes = extractMultiSwipes(data, type); @@ -4767,10 +4784,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro else { // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. if (originalType !== 'continue') { - ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes)); + ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning)); } else { - ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes)); + ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning)); } // This relies on `saveReply` having been called to add the message to the chat, so it must be last. @@ -5649,42 +5666,65 @@ function parseAndSaveLogprobs(data, continueFrom) { } /** - * Extracts the message from the response data. - * @param {object} data Response data - * @returns {string} Extracted message + * Gets the text context from the response data. + * @param {object} data Response JSON data + * @returns {string} Extracted text */ -function extractMessageFromData(data) { +function getTextContextFromData(data) { if (typeof data === 'string') { return data; } - function getTextContext() { - switch (main_api) { - case 'kobold': - return data.results[0].text; - case 'koboldhorde': - return data.text; - case 'textgenerationwebui': - return data.choices?.[0]?.text ?? data.content ?? data.response ?? ''; - case 'novel': - return data.output; - case 'openai': - return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? ''; - default: - return ''; - } + switch (main_api) { + case 'kobold': + return data.results[0].text; + case 'koboldhorde': + return data.text; + case 'textgenerationwebui': + return data.choices?.[0]?.text ?? data.content ?? data.response ?? ''; + case 'novel': + return data.output; + case 'openai': + return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? ''; + default: + return ''; } +} - const content = getTextContext(); +/** + * Extracts the message from the response data. + * @param {object} data Response data + * @returns {string} Extracted message + */ +function extractMessageFromData(data){ + const content = String(getTextContextFromData(data) ?? ''); - if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) { - const thoughts = data?.choices?.[0]?.message?.reasoning_content ?? ''; - return [thoughts, content].filter(x => x).join('\n\n'); + if (content.includes(THINK_BREAK)) { + return content.split(THINK_BREAK)[1]; } return content; } +/** + * Extracts the reasoning from the response data. + * @param {object} data Response data + * @returns {string} Extracted reasoning + */ +function extractReasoningFromData(data) { + const content = String(getTextContextFromData(data) ?? ''); + + if (content.includes(THINK_BREAK)) { + return content.split(THINK_BREAK)[0]; + } + + if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) { + return data?.choices?.[0]?.message?.reasoning_content ?? ''; + } + + return ''; +} + /** * Extracts multiswipe swipes from the response data. * @param {Object} data Response data @@ -5865,7 +5905,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc return getMessage; } -export async function saveReply(type, getMessage, fromStreaming, title, swipes) { +export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) { if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined || chat[chat.length - 1]['is_user'])) { type = 'normal'; @@ -5890,6 +5930,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes) chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); + chat[chat.length - 1]['extra']['reasoning'] = reasoning; if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } @@ -5910,6 +5951,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes) chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); + chat[chat.length - 1]['extra']['reasoning'] += reasoning; if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } @@ -5927,6 +5969,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes) chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); + chat[chat.length - 1]['extra']['reasoning'] += reasoning; if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } @@ -5944,6 +5987,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes) chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); + chat[chat.length - 1]['extra']['reasoning'] = reasoning; if (power_user.trim_spaces) { getMessage = getMessage.trim(); } @@ -8646,6 +8690,7 @@ const swipe_right = () => { // resets the timer swipeMessage.find('.mes_timer').html(''); swipeMessage.find('.tokenCounterDisplay').text(''); + swipeMessage.find('.mes_reasoning').html(''); } else { //console.log('showing previously generated swipe candidate, or "..."'); //console.log('onclick right swipe calling addOneMessage'); diff --git a/public/scripts/constants.js b/public/scripts/constants.js index f95a8e146..935a74219 100644 --- a/public/scripts/constants.js +++ b/public/scripts/constants.js @@ -14,3 +14,8 @@ export const debounce_timeout = { /** [5 sec] For delayed tasks, like auto-saving or completing batch operations that need a significant pause. */ extended: 5000, }; + +/** + * Custom boundary for splitting the text between the model's reasoning and the actual response. + */ +export const THINK_BREAK = '##�THINK_BREAK�##'; diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js index 6efadce87..65d47fc4b 100644 --- a/public/scripts/kai-settings.js +++ b/public/scripts/kai-settings.js @@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) { if (data?.token) { text += data.token; } - yield { text, swipes: [], toolCalls: [] }; + yield { text, swipes: [], toolCalls: [], state: {} }; } }; } diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js index f95e7d9f6..91ff09ef6 100644 --- a/public/scripts/nai-settings.js +++ b/public/scripts/nai-settings.js @@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) { text += data.token; } - yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] }; + yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} }; } }; } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 54e9f6125..5585f82ef 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -2095,7 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) { let text = ''; const swipes = []; const toolCalls = []; - const state = {}; + const state = { reasoning: '' }; while (true) { const { done, value } = await reader.read(); if (done) return; @@ -2113,7 +2113,7 @@ async function sendOpenAIRequest(type, messages, signal) { ToolManager.parseToolCalls(toolCalls, parsed); - yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls }; + yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state }; } }; } @@ -2150,16 +2150,17 @@ function getStreamingReply(data, state) { if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) { return data?.delta?.text || ''; } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) { - return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || ''; + if (oai_settings.show_thoughts) { + state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || ''); + } + return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || ''; } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) { return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || ''; } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) { - const hadThoughts = state.hadThoughts; - const thoughts = data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || ''; - const content = data.choices?.[0]?.delta?.content || ''; - state.hadThoughts = !!thoughts; - const separator = hadThoughts && !thoughts ? '\n\n' : ''; - return [thoughts, separator, content].filter(x => x).join('\n\n'); + if (oai_settings.show_thoughts) { + state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || ''); + } + return data.choices?.[0]?.delta?.content || ''; } else { return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; } diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index cd1004991..19b729374 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -986,6 +986,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) { let logprobs = null; const swipes = []; const toolCalls = []; + const state = {}; while (true) { const { done, value } = await reader.read(); if (done) return; @@ -1004,7 +1005,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) { logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities); } - yield { text, swipes, logprobs, toolCalls }; + yield { text, swipes, logprobs, toolCalls, state }; } }; } diff --git a/public/style.css b/public/style.css index cbfc96185..ee7cf9094 100644 --- a/public/style.css +++ b/public/style.css @@ -332,6 +332,23 @@ input[type='checkbox']:focus-visible { color: var(--SmartThemeQuoteColor); } +.mes_reasoning { + display: block; + border: 1px solid var(--SmartThemeBorderColor); + background-color: var(--black30a); + border-radius: 5px; + padding: 5px; + margin: 5px 0; + overflow-y: auto; + max-height: 100px; +} + +.mes_block:has(.edit_textarea) .mes_reasoning, +.mes_bias:empty, +.mes_reasoning:empty { + display: none; +} + .mes_text i, .mes_text em { color: var(--SmartThemeEmColor); @@ -1022,6 +1039,7 @@ body .panelControlBar { /*only affects bubblechat to make it sit nicely at the bottom*/ } +.last_mes .mes_reasoning, .last_mes .mes_text { padding-right: 30px; } @@ -1235,14 +1253,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter { overflow-y: clip; } -.mes_text { +.mes_text, +.mes_reasoning { font-weight: 500; line-height: calc(var(--mainFontSize) + .5rem); + max-width: 100%; + overflow-wrap: anywhere; +} + +.mes_text { padding-left: 0; padding-top: 5px; padding-bottom: 5px; - max-width: 100%; - overflow-wrap: anywhere; } br { diff --git a/src/constants.js b/src/constants.js index 35118a04b..faddaaf81 100644 --- a/src/constants.js +++ b/src/constants.js @@ -413,3 +413,8 @@ export const VLLM_KEYS = [ 'guided_decoding_backend', 'guided_whitespace_pattern', ]; + +/** + * Custom boundary for splitting the text between the model's reasoning and the actual response. + */ +export const THINK_BREAK = '##�THINK_BREAK�##'; diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index d67f309da..5e81b2280 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -7,6 +7,7 @@ import { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, OPENROUTER_HEADERS, + THINK_BREAK, } from '../../constants.js'; import { forwardFetchResponse, @@ -389,7 +390,7 @@ async function sendMakerSuiteRequest(request, response) { responseContent.parts = responseContent.parts.filter(part => !part.thought); } - const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n'); + const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join(THINK_BREAK); if (!responseText) { let message = 'Google AI Studio Candidate text empty'; console.log(message, generateResponseJson); From 823b9db6f6d6403354433584ecd02f14c603de68 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:41:39 +0200 Subject: [PATCH 02/83] Gemini: Fix requesting thought blocks --- src/endpoints/backends/chat-completions.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 5e81b2280..f50a35240 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -288,6 +288,7 @@ async function sendMakerSuiteRequest(request, response) { const model = String(request.body.model); const stream = Boolean(request.body.stream); const showThoughts = Boolean(request.body.show_thoughts); + const isThinking = model.includes('thinking'); const generationConfig = { stopSequences: request.body.stop, @@ -328,6 +329,12 @@ async function sendMakerSuiteRequest(request, response) { body.systemInstruction = prompt.system_instruction; } + if (isThinking && showThoughts) { + generationConfig.thinkingConfig = { + includeThoughts: true, + }; + } + return body; } @@ -341,7 +348,6 @@ async function sendMakerSuiteRequest(request, response) { controller.abort(); }); - const isThinking = model.includes('thinking'); const apiVersion = isThinking ? 'v1alpha' : 'v1beta'; const responseType = (stream ? 'streamGenerateContent' : 'generateContent'); From 144277bdcc5ac110d888510b79d0ad6a8d92873a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:43:04 +0200 Subject: [PATCH 03/83] Wrap thonk into collapsible --- public/index.html | 7 ++++++- public/style.css | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 3fe2162fb..4255bcae5 100644 --- a/public/index.html +++ b/public/index.html @@ -6229,7 +6229,12 @@
-
+
+ + Reasoning + +
+
diff --git a/public/style.css b/public/style.css index ee7cf9094..d0c528f5a 100644 --- a/public/style.css +++ b/public/style.css @@ -340,10 +340,19 @@ input[type='checkbox']:focus-visible { padding: 5px; margin: 5px 0; overflow-y: auto; - max-height: 100px; + max-height: min(50vh, 300px); } -.mes_block:has(.edit_textarea) .mes_reasoning, +.mes_reasoning_summary { + cursor: pointer; +} + +.mes_reasoning_summary > span { + margin-left: 5px; +} + +.mes_reasoning_details:has(.mes_reasoning:empty), +.mes_block:has(.edit_textarea) .mes_reasoning_details, .mes_bias:empty, .mes_reasoning:empty { display: none; From 03c98fb55ac95fb051ef98b48e6e787d54ff9eb0 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:56:44 +0200 Subject: [PATCH 04/83] OpenRouter: Support reasoning blocks --- public/index.html | 2 +- public/script.js | 23 ++++++++++------------ public/scripts/constants.js | 5 ----- public/scripts/openai.js | 5 +++++ public/scripts/sse-stream.js | 15 ++++++++++++++ src/constants.js | 5 ----- src/endpoints/backends/chat-completions.js | 13 ++++++------ 7 files changed, 37 insertions(+), 31 deletions(-) diff --git a/public/index.html b/public/index.html index a5981a186..e1b055692 100644 --- a/public/index.html +++ b/public/index.html @@ -1977,7 +1977,7 @@
-
+
+
+

+ Reasoning +

+
+ +
+
+ Prefix + +
+
+ Suffix + +
+
+
+
+ Separator + +
+
+ Max Additions + +
+
+
+

Miscellaneous

@@ -6221,11 +6254,10 @@
- + + - +
diff --git a/public/locales/fr-fr.json b/public/locales/fr-fr.json index 96124e043..342624c8a 100644 --- a/public/locales/fr-fr.json +++ b/public/locales/fr-fr.json @@ -1385,7 +1385,7 @@ "enable_functions_desc_1": "Autorise l'utilisation", "enable_functions_desc_2": "outils de fonction", "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.", - "Show model thoughts": "Afficher les pensées du modèle", + "Show model reasoning": "Afficher les pensées du modèle", "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.", "Confirm token parsing with": "Confirmer l'analyse des tokens avec", "openai_logit_bias_no_items": "Aucun élément", diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 04129749c..4df2fd60a 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -266,7 +266,7 @@ "Use system prompt": "使用系统提示词", "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过", "Merges_all_system_messages_desc_2": "字段发送。", - "Show model thoughts": "展示思维链", + "Show model reasoning": "展示思维链", "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。", "Assistant Prefill": "AI预填", "Expand the editor": "展开编辑器", diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index 5d8313c1d..f1656afe7 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -2357,7 +2357,7 @@ "Forbid": "禁止", "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。", "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。", - "Show model thoughts": "顯示模型思維鏈", + "Show model reasoning": "顯示模型思維鏈", "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。", "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]", "Model ID (optional)": "模型 ID(可選)", diff --git a/public/script.js b/public/script.js index c18d047b1..f91070709 100644 --- a/public/script.js +++ b/public/script.js @@ -238,7 +238,7 @@ import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_set import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js'; -import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js'; +import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, PromptReasoning } from './scripts/chats.js'; import { getPresetManager, initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; @@ -3844,6 +3844,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro coreChat.pop(); } + const reasoning = new PromptReasoning(); + for (let i = coreChat.length - 1; i >= 0; i--) { + coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) }; + } + coreChat = await Promise.all(coreChat.map(async (chatItem, index) => { let message = chatItem.mes; let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT; @@ -8034,7 +8039,7 @@ function updateEditArrowClasses() { } } -function closeMessageEditor() { +export function closeMessageEditor() { if (this_edit_mes_id) { $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click(); } diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 43dbb9b0c..f18f5c883 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -24,6 +24,8 @@ import { updateChatMetadata, system_message_types, updateMessageBlock, + closeMessageEditor, + substituteParams, } from '../script.js'; import { selected_group } from './group-chats.js'; import { power_user } from './power-user.js'; @@ -1418,6 +1420,47 @@ export function registerFileConverter(mimeType, converter) { converters[mimeType] = converter; } +/** + * Helper class for adding reasoning to messages. + * Keeps track of the number of reasoning additions. + */ +export class PromptReasoning { + static REASONING_PLACEHOLDER = '\u200B'; + + constructor() { + this.counter = 0; + } + + /** + * Add reasoning to a message according to the power user settings. + * @param {string} content Message content + * @param {string} reasoning Message reasoning + * @returns {string} Message content with reasoning + */ + addToMessage(content, reasoning) { + // Disabled or reached limit of additions + if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) { + return content; + } + + // No reasoning provided or a placeholder + if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { + return content; + } + + // Increment the counter + this.counter++; + + // Substitute macros in variable parts + const prefix = substituteParams(power_user.reasoning.prefix || ''); + const separator = substituteParams(power_user.reasoning.separator || ''); + const suffix = substituteParams(power_user.reasoning.suffix || ''); + + // Combine parts with reasoning and content + return `${prefix}${reasoning}${suffix}${separator}${content}`; + } +} + jQuery(function () { $(document).on('click', '.mes_hide', async function () { const messageBlock = $(this).closest('.mes'); @@ -1574,6 +1617,25 @@ jQuery(function () { e.preventDefault(); }); + $(document).on('click', '.mes_edit_add_reasoning', async function () { + const mesBlock = $(this).closest('.mes'); + const mesId = Number(mesBlock.attr('mesid')); + const message = chat[mesId]; + if (!message?.extra){ + return; + } + + if (message.extra.reasoning) { + toastr.info(t`Reasoning already exists.`, t`Edit Message`); + return; + } + + message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; + await saveChatConditional(); + closeMessageEditor(); + updateMessageBlock(mesId, message); + }); + $(document).on('click', '.mes_reasoning_delete', async function (e) { e.stopPropagation(); e.preventDefault(); diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index bfe8eca2e..4859c0c71 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -253,6 +253,14 @@ let power_user = { content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.', }, + reasoning: { + add_to_prompts: false, + prefix: '\n', + suffix: '\n', + separator: '\n', + max_additions: 1, + }, + personas: {}, default_persona: null, persona_descriptions: {}, @@ -1613,6 +1621,7 @@ async function loadPowerUserSettings(settings, data) { loadMovingUIState(); loadCharListState(); toggleMDHotkeyIconDisplay(); + loadReasoningSettings(); } function toggleMDHotkeyIconDisplay() { @@ -1629,6 +1638,38 @@ function loadCharListState() { document.body.classList.toggle('charListGrid', power_user.charListGrid); } +function loadReasoningSettings() { + $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts); + $('#reasoning_add_to_prompts').on('change', function () { + power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + + $('#reasoning_prefix').val(power_user.reasoning.prefix); + $('#reasoning_prefix').on('input', function () { + power_user.reasoning.prefix = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_suffix').val(power_user.reasoning.suffix); + $('#reasoning_suffix').on('input', function () { + power_user.reasoning.suffix = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_separator').val(power_user.reasoning.separator); + $('#reasoning_separator').on('input', function () { + power_user.reasoning.separator = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_max_additions').val(power_user.reasoning.max_additions); + $('#reasoning_max_additions').on('input', function () { + power_user.reasoning.max_additions = Number($(this).val()); + saveSettingsDebounced(); + }); +} + function loadMovingUIState() { if (!isMobile() && power_user.movingUIState From 8fc880b69b88667f8120266669369dad2921e73e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 02:07:50 +0200 Subject: [PATCH 14/83] Early stopping if prompt reasoning limit reached --- public/script.js | 3 +++ public/scripts/chats.js | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/public/script.js b/public/script.js index f91070709..b0ed60704 100644 --- a/public/script.js +++ b/public/script.js @@ -3846,6 +3846,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro const reasoning = new PromptReasoning(); for (let i = coreChat.length - 1; i >= 0; i--) { + if (reasoning.isLimitReached()) { + break; + } coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) }; } diff --git a/public/scripts/chats.js b/public/scripts/chats.js index f18f5c883..ef1db0732 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1431,6 +1431,18 @@ export class PromptReasoning { this.counter = 0; } + /** + * Checks if the limit of reasoning additions has been reached. + * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise. + */ + isLimitReached() { + if (!power_user.reasoning.add_to_prompts) { + return true; + } + + return this.counter >= power_user.reasoning.max_additions; + } + /** * Add reasoning to a message according to the power user settings. * @param {string} content Message content From 45d4d1bb3ef2f544f0d652f598d590067dd32348 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 02:49:10 +0200 Subject: [PATCH 15/83] [wip] Open reasoning editor --- public/index.html | 3 +++ public/script.js | 2 +- public/scripts/chats.js | 48 ++++++++++++++++++++++++++++++----------- public/style.css | 27 +++++++++++++---------- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/public/index.html b/public/index.html index 7bb533dc7..c7cc4c617 100644 --- a/public/index.html +++ b/public/index.html @@ -6265,6 +6265,9 @@ Reasoning
+
+
+
diff --git a/public/script.js b/public/script.js index b0ed60704..b4fd28f2f 100644 --- a/public/script.js +++ b/public/script.js @@ -11274,7 +11274,7 @@ jQuery(async function () { $(document).keyup(function (e) { if (e.key === 'Escape') { - const isEditVisible = $('#curEditTextarea').is(':visible'); + const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length; if (isEditVisible && power_user.auto_save_msg_edits === false) { closeMessageEditor(); $('#send_textarea').focus(); diff --git a/public/scripts/chats.js b/public/scripts/chats.js index ef1db0732..5b2db51c5 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1474,6 +1474,18 @@ export class PromptReasoning { } jQuery(function () { + /** + * Gets a message from a jQuery element. + * @param {Element} element + * @returns {{messageId: number, message: object, messageBlock: JQuery}} + */ + const getMessageFromJquery = function (element) { + const messageBlock = $(element).closest('.mes'); + const messageId = Number(messageBlock.attr('mesid')); + const message = chat[messageId]; + return { messageId: messageId, message, messageBlock }; + }; + $(document).on('click', '.mes_hide', async function () { const messageBlock = $(this).closest('.mes'); const messageId = Number(messageBlock.attr('mesid')); @@ -1629,11 +1641,25 @@ jQuery(function () { e.preventDefault(); }); + $(document).on('click', '.mes_reasoning_edit', function (e) { + e.stopPropagation(); + e.preventDefault(); + const { message, messageBlock } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + const reasoning = message?.extra?.reasoning; + const textarea = document.createElement('textarea'); + const reasoningBlock = messageBlock.find('.mes_reasoning'); + textarea.classList.add('reasoning_edit_textarea'); + textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; + $(textarea).insertBefore(reasoningBlock); + }); + $(document).on('click', '.mes_edit_add_reasoning', async function () { - const mesBlock = $(this).closest('.mes'); - const mesId = Number(mesBlock.attr('mesid')); - const message = chat[mesId]; - if (!message?.extra){ + const { message, messageId } = getMessageFromJquery(this); + if (!message?.extra) { return; } @@ -1645,7 +1671,7 @@ jQuery(function () { message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; await saveChatConditional(); closeMessageEditor(); - updateMessageBlock(mesId, message); + updateMessageBlock(messageId, message); }); $(document).on('click', '.mes_reasoning_delete', async function (e) { @@ -1658,21 +1684,17 @@ jQuery(function () { return; } - const mesBlock = $(this).closest('.mes'); - const mesId = Number(mesBlock.attr('mesid')); - const message = chat[mesId]; - if (!message?.extra){ + const { message, messageId } = getMessageFromJquery(this); + if (!message?.extra) { return; } message.extra.reasoning = ''; await saveChatConditional(); - updateMessageBlock(mesId, message); + updateMessageBlock(messageId, message); }); $(document).on('pointerup', '.mes_reasoning_copy', async function () { - const mesBlock = $(this).closest('.mes'); - const mesId = Number(mesBlock.attr('mesid')); - const message = chat[mesId]; + const { message } = getMessageFromJquery(this); const reasoning = message?.extra?.reasoning; if (!reasoning) { diff --git a/public/style.css b/public/style.css index 2b7ce0a01..bd31aba7f 100644 --- a/public/style.css +++ b/public/style.css @@ -353,9 +353,18 @@ input[type='checkbox']:focus-visible { .mes_reasoning_summary { cursor: pointer; position: relative; + margin: 2px; } -.mes_reasoning_details:not([open]) .mes_reasoning_actions { +.mes_bias:empty, +.mes_reasoning:empty, +.mes_reasoning_details:has(.mes_reasoning:empty), +.mes_block:has(.edit_textarea) .mes_reasoning_details, +.mes_reasoning_details:not([open]) .mes_reasoning_actions, +.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning, +.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done, +.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel, +.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) { display: none; } @@ -373,21 +382,14 @@ input[type='checkbox']:focus-visible { padding: 1px; } -.mes_reasoning_summary > span { +.mes_reasoning_summary>span { margin-left: 0.5em; } -.mes_reasoning_details:has(.mes_reasoning:empty), -.mes_block:has(.edit_textarea) .mes_reasoning_details, -.mes_bias:empty, -.mes_reasoning:empty { - display: none; -} - .mes_text i, .mes_text em, .mes_reasoning i, -.mes_reasoning em { +.mes_reasoning em { color: var(--SmartThemeEmColor); } @@ -408,7 +410,7 @@ input[type='checkbox']:focus-visible { .mes_reasoning font[color] em, .mes_reasoning font[color] i, .mes_reasoning font[color] u, -.mes_reasoning font[color] q { +.mes_reasoning font[color] q { color: inherit; } @@ -4213,10 +4215,12 @@ input[type="range"]::-webkit-slider-thumb { align-items: center; } +.mes_reasoning_edit_cancel, .mes_edit_cancel.menu_button { background-color: var(--crimson70a); } +.mes_reasoning_edit_done, .mes_edit_done.menu_button { background-color: var(--okGreen70a); } @@ -4225,6 +4229,7 @@ input[type="range"]::-webkit-slider-thumb { opacity: 1; } +.reasoning_edit_textarea, .edit_textarea { padding: 5px; margin: 0; From 17d4175b47bb274048f2503ecb8e423836265308 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 05:14:17 +0200 Subject: [PATCH 16/83] Functional reasoning edit --- public/script.js | 27 +++++++++++++++++----- public/scripts/chats.js | 50 +++++++++++++++++++++++++++++++++++++++++ public/style.css | 10 +++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/public/script.js b/public/script.js index b4fd28f2f..037ff7bc3 100644 --- a/public/script.js +++ b/public/script.js @@ -5686,7 +5686,7 @@ function parseAndSaveLogprobs(data, continueFrom) { * @param {object} data Response data * @returns {string} Extracted message */ -function extractMessageFromData(data){ +function extractMessageFromData(data) { if (typeof data === 'string') { return data; } @@ -8042,9 +8042,23 @@ function updateEditArrowClasses() { } } -export function closeMessageEditor() { - if (this_edit_mes_id) { - $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click(); +/** + * Closes the message editor. + * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'. + */ +export function closeMessageEditor(what = 'all') { + if (what === 'message' || what === 'all') { + if (this_edit_mes_id) { + $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click(); + } + } + if (what === 'reasoning' || what === 'all') { + document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => { + const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel'); + if (cancelButton instanceof HTMLElement) { + cancelButton.click(); + } + }); } } @@ -11274,14 +11288,15 @@ jQuery(async function () { $(document).keyup(function (e) { if (e.key === 'Escape') { - const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length; + const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0; if (isEditVisible && power_user.auto_save_msg_edits === false) { - closeMessageEditor(); + closeMessageEditor('all'); $('#send_textarea').focus(); return; } if (isEditVisible && power_user.auto_save_msg_edits === true) { $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click(); + closeMessageEditor('reasoning'); $('#send_textarea').focus(); return; } diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 5b2db51c5..8d6ec6040 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1650,11 +1650,61 @@ jQuery(function () { } const reasoning = message?.extra?.reasoning; + const chatElement = document.getElementById('chat'); const textarea = document.createElement('textarea'); const reasoningBlock = messageBlock.find('.mes_reasoning'); textarea.classList.add('reasoning_edit_textarea'); textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; $(textarea).insertBefore(reasoningBlock); + + if (!CSS.supports('field-sizing', 'content')) { + const resetHeight = function () { + const scrollTop = chatElement.scrollTop; + textarea.style.height = '0px'; + textarea.style.height = `${textarea.scrollHeight}px`; + chatElement.scrollTop = scrollTop; + }; + + textarea.addEventListener('input', resetHeight); + resetHeight(); + } + + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + + const textareaRect = textarea.getBoundingClientRect(); + const chatRect = chatElement.getBoundingClientRect(); + + // Scroll if textarea bottom is below visible area + if (textareaRect.bottom > chatRect.bottom) { + const scrollOffset = textareaRect.bottom - chatRect.bottom; + chatElement.scrollTop += scrollOffset; + } + }); + + $(document).on('click', '.mes_reasoning_edit_done', async function (e) { + e.stopPropagation(); + e.preventDefault(); + const { message, messageId, messageBlock } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + const textarea = messageBlock.find('.reasoning_edit_textarea'); + const reasoning = String(textarea.val()); + message.extra.reasoning = reasoning; + await saveChatConditional(); + updateMessageBlock(messageId, message); + textarea.remove(); + }); + + $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { + e.stopPropagation(); + e.preventDefault(); + + const { messageBlock } = getMessageFromJquery(this); + const textarea = messageBlock.find('.reasoning_edit_textarea'); + textarea.remove(); }); $(document).on('click', '.mes_edit_add_reasoning', async function () { diff --git a/public/style.css b/public/style.css index bd31aba7f..0318e6d7d 100644 --- a/public/style.css +++ b/public/style.css @@ -356,6 +356,12 @@ input[type='checkbox']:focus-visible { margin: 2px; } +@supports not selector(:has(*)) { + .mes_reasoning_details { + display: none !important; + } +} + .mes_bias:empty, .mes_reasoning:empty, .mes_reasoning_details:has(.mes_reasoning:empty), @@ -1089,6 +1095,10 @@ body .panelControlBar { /*only affects bubblechat to make it sit nicely at the bottom*/ } +.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) { + margin-bottom: 30px; +} + .last_mes .mes_reasoning, .last_mes .mes_text { padding-right: 30px; From eb798fa4f191892bffe07d40bd255b69dddf2d4f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 16:47:13 +0200 Subject: [PATCH 17/83] Move reasoning-specific code into its own module --- public/script.js | 4 +- public/scripts/chats.js | 188 ----------------------------- public/scripts/power-user.js | 33 ----- public/scripts/reasoning.js | 228 +++++++++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 222 deletions(-) create mode 100644 public/scripts/reasoning.js diff --git a/public/script.js b/public/script.js index 037ff7bc3..43245e7a3 100644 --- a/public/script.js +++ b/public/script.js @@ -238,7 +238,7 @@ import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_set import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js'; -import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, PromptReasoning } from './scripts/chats.js'; +import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js'; import { getPresetManager, initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; @@ -267,6 +267,7 @@ import { initSettingsSearch } from './scripts/setting-search.js'; import { initBulkEdit } from './scripts/bulk-edit.js'; import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; import { getContext } from './scripts/st-context.js'; +import { initReasoning, PromptReasoning } from './scripts/reasoning.js'; // API OBJECT FOR EXTERNAL WIRING globalThis.SillyTavern = { @@ -982,6 +983,7 @@ async function firstLoadInit() { initServerHistory(); initSettingsSearch(); initBulkEdit(); + initReasoning(); await initScrapers(); doDailyExtensionUpdatesCheck(); await hideLoader(); diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 8d6ec6040..94ad882bb 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -23,9 +23,6 @@ import { neutralCharacterName, updateChatMetadata, system_message_types, - updateMessageBlock, - closeMessageEditor, - substituteParams, } from '../script.js'; import { selected_group } from './group-chats.js'; import { power_user } from './power-user.js'; @@ -40,7 +37,6 @@ import { saveBase64AsFile, extractTextFromOffice, download, - copyText, } from './utils.js'; import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; @@ -1420,72 +1416,7 @@ export function registerFileConverter(mimeType, converter) { converters[mimeType] = converter; } -/** - * Helper class for adding reasoning to messages. - * Keeps track of the number of reasoning additions. - */ -export class PromptReasoning { - static REASONING_PLACEHOLDER = '\u200B'; - - constructor() { - this.counter = 0; - } - - /** - * Checks if the limit of reasoning additions has been reached. - * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise. - */ - isLimitReached() { - if (!power_user.reasoning.add_to_prompts) { - return true; - } - - return this.counter >= power_user.reasoning.max_additions; - } - - /** - * Add reasoning to a message according to the power user settings. - * @param {string} content Message content - * @param {string} reasoning Message reasoning - * @returns {string} Message content with reasoning - */ - addToMessage(content, reasoning) { - // Disabled or reached limit of additions - if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) { - return content; - } - - // No reasoning provided or a placeholder - if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { - return content; - } - - // Increment the counter - this.counter++; - - // Substitute macros in variable parts - const prefix = substituteParams(power_user.reasoning.prefix || ''); - const separator = substituteParams(power_user.reasoning.separator || ''); - const suffix = substituteParams(power_user.reasoning.suffix || ''); - - // Combine parts with reasoning and content - return `${prefix}${reasoning}${suffix}${separator}${content}`; - } -} - jQuery(function () { - /** - * Gets a message from a jQuery element. - * @param {Element} element - * @returns {{messageId: number, message: object, messageBlock: JQuery}} - */ - const getMessageFromJquery = function (element) { - const messageBlock = $(element).closest('.mes'); - const messageId = Number(messageBlock.attr('mesid')); - const message = chat[messageId]; - return { messageId: messageId, message, messageBlock }; - }; - $(document).on('click', '.mes_hide', async function () { const messageBlock = $(this).closest('.mes'); const messageId = Number(messageBlock.attr('mesid')); @@ -1636,125 +1567,6 @@ jQuery(function () { $(document).on('click', '.mes_img_enlarge', enlargeMessageImage); $(document).on('click', '.mes_img_delete', deleteMessageImage); - $(document).on('click', '.mes_reasoning_copy', (e) => { - e.stopPropagation(); - e.preventDefault(); - }); - - $(document).on('click', '.mes_reasoning_edit', function (e) { - e.stopPropagation(); - e.preventDefault(); - const { message, messageBlock } = getMessageFromJquery(this); - if (!message?.extra) { - return; - } - - const reasoning = message?.extra?.reasoning; - const chatElement = document.getElementById('chat'); - const textarea = document.createElement('textarea'); - const reasoningBlock = messageBlock.find('.mes_reasoning'); - textarea.classList.add('reasoning_edit_textarea'); - textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; - $(textarea).insertBefore(reasoningBlock); - - if (!CSS.supports('field-sizing', 'content')) { - const resetHeight = function () { - const scrollTop = chatElement.scrollTop; - textarea.style.height = '0px'; - textarea.style.height = `${textarea.scrollHeight}px`; - chatElement.scrollTop = scrollTop; - }; - - textarea.addEventListener('input', resetHeight); - resetHeight(); - } - - textarea.focus(); - textarea.setSelectionRange(textarea.value.length, textarea.value.length); - - const textareaRect = textarea.getBoundingClientRect(); - const chatRect = chatElement.getBoundingClientRect(); - - // Scroll if textarea bottom is below visible area - if (textareaRect.bottom > chatRect.bottom) { - const scrollOffset = textareaRect.bottom - chatRect.bottom; - chatElement.scrollTop += scrollOffset; - } - }); - - $(document).on('click', '.mes_reasoning_edit_done', async function (e) { - e.stopPropagation(); - e.preventDefault(); - const { message, messageId, messageBlock } = getMessageFromJquery(this); - if (!message?.extra) { - return; - } - - const textarea = messageBlock.find('.reasoning_edit_textarea'); - const reasoning = String(textarea.val()); - message.extra.reasoning = reasoning; - await saveChatConditional(); - updateMessageBlock(messageId, message); - textarea.remove(); - }); - - $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { - e.stopPropagation(); - e.preventDefault(); - - const { messageBlock } = getMessageFromJquery(this); - const textarea = messageBlock.find('.reasoning_edit_textarea'); - textarea.remove(); - }); - - $(document).on('click', '.mes_edit_add_reasoning', async function () { - const { message, messageId } = getMessageFromJquery(this); - if (!message?.extra) { - return; - } - - if (message.extra.reasoning) { - toastr.info(t`Reasoning already exists.`, t`Edit Message`); - return; - } - - message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; - await saveChatConditional(); - closeMessageEditor(); - updateMessageBlock(messageId, message); - }); - - $(document).on('click', '.mes_reasoning_delete', async function (e) { - e.stopPropagation(); - 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.`); - - if (!confirm) { - return; - } - - const { message, messageId } = getMessageFromJquery(this); - if (!message?.extra) { - return; - } - message.extra.reasoning = ''; - await saveChatConditional(); - updateMessageBlock(messageId, message); - }); - - $(document).on('pointerup', '.mes_reasoning_copy', async function () { - const { message } = getMessageFromJquery(this); - const reasoning = message?.extra?.reasoning; - - if (!reasoning) { - return; - } - - await copyText(reasoning); - toastr.info(t`Copied!`, '', { timeOut: 2000 }); - }); - $('#file_form_input').on('change', async () => { const fileInput = document.getElementById('file_form_input'); if (!(fileInput instanceof HTMLInputElement)) return; diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 4859c0c71..08840c7bc 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -1621,7 +1621,6 @@ async function loadPowerUserSettings(settings, data) { loadMovingUIState(); loadCharListState(); toggleMDHotkeyIconDisplay(); - loadReasoningSettings(); } function toggleMDHotkeyIconDisplay() { @@ -1638,38 +1637,6 @@ function loadCharListState() { document.body.classList.toggle('charListGrid', power_user.charListGrid); } -function loadReasoningSettings() { - $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts); - $('#reasoning_add_to_prompts').on('change', function () { - power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); - saveSettingsDebounced(); - }); - - $('#reasoning_prefix').val(power_user.reasoning.prefix); - $('#reasoning_prefix').on('input', function () { - power_user.reasoning.prefix = String($(this).val()); - saveSettingsDebounced(); - }); - - $('#reasoning_suffix').val(power_user.reasoning.suffix); - $('#reasoning_suffix').on('input', function () { - power_user.reasoning.suffix = String($(this).val()); - saveSettingsDebounced(); - }); - - $('#reasoning_separator').val(power_user.reasoning.separator); - $('#reasoning_separator').on('input', function () { - power_user.reasoning.separator = String($(this).val()); - saveSettingsDebounced(); - }); - - $('#reasoning_max_additions').val(power_user.reasoning.max_additions); - $('#reasoning_max_additions').on('input', function () { - power_user.reasoning.max_additions = Number($(this).val()); - saveSettingsDebounced(); - }); -} - function loadMovingUIState() { if (!isMobile() && power_user.movingUIState diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js new file mode 100644 index 000000000..0d2011b12 --- /dev/null +++ b/public/scripts/reasoning.js @@ -0,0 +1,228 @@ +import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; +import { t } from './i18n.js'; +import { Popup } from './popup.js'; +import { power_user } from './power-user.js'; +import { copyText } from './utils.js'; + +/** + * Gets a message from a jQuery element. + * @param {Element} element + * @returns {{messageId: number, message: object, messageBlock: JQuery}} + */ +function getMessageFromJquery(element) { + const messageBlock = $(element).closest('.mes'); + const messageId = Number(messageBlock.attr('mesid')); + const message = chat[messageId]; + return { messageId: messageId, message, messageBlock }; +} + +/** + * Helper class for adding reasoning to messages. + * Keeps track of the number of reasoning additions. + */ +export class PromptReasoning { + static REASONING_PLACEHOLDER = '\u200B'; + + constructor() { + this.counter = 0; + } + + /** + * Checks if the limit of reasoning additions has been reached. + * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise. + */ + isLimitReached() { + if (!power_user.reasoning.add_to_prompts) { + return true; + } + + return this.counter >= power_user.reasoning.max_additions; + } + + /** + * Add reasoning to a message according to the power user settings. + * @param {string} content Message content + * @param {string} reasoning Message reasoning + * @returns {string} Message content with reasoning + */ + addToMessage(content, reasoning) { + // Disabled or reached limit of additions + if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) { + return content; + } + + // No reasoning provided or a placeholder + if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { + return content; + } + + // Increment the counter + this.counter++; + + // Substitute macros in variable parts + const prefix = substituteParams(power_user.reasoning.prefix || ''); + const separator = substituteParams(power_user.reasoning.separator || ''); + const suffix = substituteParams(power_user.reasoning.suffix || ''); + + // Combine parts with reasoning and content + return `${prefix}${reasoning}${suffix}${separator}${content}`; + } +} + +function loadReasoningSettings() { + $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts); + $('#reasoning_add_to_prompts').on('change', function () { + power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + + $('#reasoning_prefix').val(power_user.reasoning.prefix); + $('#reasoning_prefix').on('input', function () { + power_user.reasoning.prefix = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_suffix').val(power_user.reasoning.suffix); + $('#reasoning_suffix').on('input', function () { + power_user.reasoning.suffix = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_separator').val(power_user.reasoning.separator); + $('#reasoning_separator').on('input', function () { + power_user.reasoning.separator = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_max_additions').val(power_user.reasoning.max_additions); + $('#reasoning_max_additions').on('input', function () { + power_user.reasoning.max_additions = Number($(this).val()); + saveSettingsDebounced(); + }); +} + +function setReasoningEventHandlers(){ + $(document).on('click', '.mes_reasoning_copy', (e) => { + e.stopPropagation(); + e.preventDefault(); + }); + + $(document).on('click', '.mes_reasoning_edit', function (e) { + e.stopPropagation(); + e.preventDefault(); + const { message, messageBlock } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + const reasoning = message?.extra?.reasoning; + const chatElement = document.getElementById('chat'); + const textarea = document.createElement('textarea'); + const reasoningBlock = messageBlock.find('.mes_reasoning'); + textarea.classList.add('reasoning_edit_textarea'); + textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; + $(textarea).insertBefore(reasoningBlock); + + if (!CSS.supports('field-sizing', 'content')) { + const resetHeight = function () { + const scrollTop = chatElement.scrollTop; + textarea.style.height = '0px'; + textarea.style.height = `${textarea.scrollHeight}px`; + chatElement.scrollTop = scrollTop; + }; + + textarea.addEventListener('input', resetHeight); + resetHeight(); + } + + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + + const textareaRect = textarea.getBoundingClientRect(); + const chatRect = chatElement.getBoundingClientRect(); + + // Scroll if textarea bottom is below visible area + if (textareaRect.bottom > chatRect.bottom) { + const scrollOffset = textareaRect.bottom - chatRect.bottom; + chatElement.scrollTop += scrollOffset; + } + }); + + $(document).on('click', '.mes_reasoning_edit_done', async function (e) { + e.stopPropagation(); + e.preventDefault(); + const { message, messageId, messageBlock } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + const textarea = messageBlock.find('.reasoning_edit_textarea'); + const reasoning = String(textarea.val()); + message.extra.reasoning = reasoning; + await saveChatConditional(); + updateMessageBlock(messageId, message); + textarea.remove(); + }); + + $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { + e.stopPropagation(); + e.preventDefault(); + + const { messageBlock } = getMessageFromJquery(this); + const textarea = messageBlock.find('.reasoning_edit_textarea'); + textarea.remove(); + }); + + $(document).on('click', '.mes_edit_add_reasoning', async function () { + const { message, messageId } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + if (message.extra.reasoning) { + toastr.info(t`Reasoning already exists.`, t`Edit Message`); + return; + } + + message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; + await saveChatConditional(); + closeMessageEditor(); + updateMessageBlock(messageId, message); + }); + + $(document).on('click', '.mes_reasoning_delete', async function (e) { + e.stopPropagation(); + 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.`); + + if (!confirm) { + return; + } + + const { message, messageId } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + message.extra.reasoning = ''; + await saveChatConditional(); + updateMessageBlock(messageId, message); + }); + + $(document).on('pointerup', '.mes_reasoning_copy', async function () { + const { message } = getMessageFromJquery(this); + const reasoning = message?.extra?.reasoning; + + if (!reasoning) { + return; + } + + await copyText(reasoning); + toastr.info(t`Copied!`, '', { timeOut: 2000 }); + }); +} + +export function initReasoning() { + loadReasoningSettings(); + setReasoningEventHandlers(); +} From c9ab987658d4d961a157d7f235cfd317ad334996 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 16:48:04 +0200 Subject: [PATCH 18/83] Fix default thonk separator --- public/scripts/power-user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 08840c7bc..2a74f8722 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -257,7 +257,7 @@ let power_user = { add_to_prompts: false, prefix: '\n', suffix: '\n', - separator: '\n', + separator: '\n\n', max_additions: 1, }, From a7516937f78108f5cfa589a4f5869e8c92ef5e0d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:00:14 +0200 Subject: [PATCH 19/83] Add reasoning slash commands --- public/scripts/reasoning.js | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index 0d2011b12..90721cd5d 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -1,7 +1,12 @@ import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; import { t } from './i18n.js'; +import { MacrosParser } from './macros.js'; import { Popup } from './popup.js'; import { power_user } from './power-user.js'; +import { SlashCommand } from './slash-commands/SlashCommand.js'; +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; +import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { copyText } from './utils.js'; /** @@ -101,6 +106,67 @@ function loadReasoningSettings() { }); } +function registerReasoningSlashCommands() { + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'reasoning-get', + returns: ARGUMENT_TYPE.STRING, + helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Message ID. If not provided, the message ID of the last message is used.', + typeList: ARGUMENT_TYPE.NUMBER, + enumProvider: commonEnumProviders.messages(), + }), + ], + callback: (_args, value) => { + const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1; + const message = chat[messageId]; + const reasoning = message?.extra?.reasoning; + return reasoning !== PromptReasoning.REASONING_PLACEHOLDER ? reasoning : ''; + }, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'reasoning-set', + returns: ARGUMENT_TYPE.STRING, + helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'at', + description: 'Message ID. If not provided, the message ID of the last message is used.', + typeList: ARGUMENT_TYPE.NUMBER, + enumProvider: commonEnumProviders.messages(), + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Reasoning block content.', + typeList: ARGUMENT_TYPE.STRING, + }), + ], + callback: async (args, value) => { + const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1; + const message = chat[messageId]; + if (!message?.extra) { + return ''; + } + + message.extra.reasoning = String(value); + await saveChatConditional(); + + closeMessageEditor('reasoning'); + updateMessageBlock(messageId, message); + return message.extra.reasoning; + }, + })); +} + +function registerReasoningMacros() { + MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`); + MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`); + MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`); +} + function setReasoningEventHandlers(){ $(document).on('click', '.mes_reasoning_copy', (e) => { e.stopPropagation(); @@ -225,4 +291,6 @@ function setReasoningEventHandlers(){ export function initReasoning() { loadReasoningSettings(); setReasoningEventHandlers(); + registerReasoningSlashCommands(); + registerReasoningMacros(); } From bfedf20db52c1e91ab5e141e12a93d2c220b982d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:29:31 +0200 Subject: [PATCH 20/83] Add reasoning tokens to token count. --- public/script.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/public/script.js b/public/script.js index 43245e7a3..38a4d562f 100644 --- a/public/script.js +++ b/public/script.js @@ -3195,7 +3195,8 @@ class StreamingProcessor { this.#updateMessageBlockVisibility(); const currentTime = new Date(); // Don't waste time calculating token count for streaming - const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0; + const tokenCountText = (this.reasoning || '') + processedText; + const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0; const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount); chat[messageId]['mes'] = processedText; chat[messageId]['gen_started'] = this.timeStarted; @@ -5936,7 +5937,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); chat[chat.length - 1]['extra']['reasoning'] = reasoning; if (power_user.message_token_count_enabled) { - chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); + const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; + chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); @@ -5957,7 +5959,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); chat[chat.length - 1]['extra']['reasoning'] += reasoning; if (power_user.message_token_count_enabled) { - chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); + const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; + chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); @@ -5975,7 +5978,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); chat[chat.length - 1]['extra']['reasoning'] += reasoning; if (power_user.message_token_count_enabled) { - chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); + const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; + chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); @@ -6001,7 +6005,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, chat[chat.length - 1]['gen_finished'] = generationFinished; if (power_user.message_token_count_enabled) { - chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); + const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; + chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); } if (selected_group) { @@ -8544,7 +8549,8 @@ function swipe_left() { // when we swipe left..but no generation. } const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`); - const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0); + const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes; + const tokenCount = await getTokenCountAsync(tokenCountText, 0); chat[chat.length - 1]['extra']['token_count'] = tokenCount; swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`); } @@ -8719,7 +8725,8 @@ const swipe_right = () => { chat[chat.length - 1].extra = {}; } - const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0); + const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes; + const tokenCount = await getTokenCountAsync(tokenCountText, 0); chat[chat.length - 1]['extra']['token_count'] = tokenCount; swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`); } @@ -9521,7 +9528,8 @@ function addDebugFunctions() { message.extra = {}; } - message.extra.token_count = await getTokenCountAsync(message.mes, 0); + const tokenCountText = (message?.extra?.reasoning || '') + message.mes; + message.extra.token_count = await getTokenCountAsync(tokenCountText, 0); } await saveChatConditional(); From fd1fdc646665d733bd2179d9439391c3c26ce84d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:48:50 +0000 Subject: [PATCH 21/83] Fix fitting class resetting after picking BG --- public/scripts/backgrounds.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/scripts/backgrounds.js b/public/scripts/backgrounds.js index f83d4e044..683e9c4ee 100644 --- a/public/scripts/backgrounds.js +++ b/public/scripts/backgrounds.js @@ -482,10 +482,10 @@ function highlightNewBackground(bg) { */ function setFittingClass(fitting) { const backgrounds = $('#bg1, #bg_custom'); - backgrounds.toggleClass('cover', fitting === 'cover'); - backgrounds.toggleClass('contain', fitting === 'contain'); - backgrounds.toggleClass('stretch', fitting === 'stretch'); - backgrounds.toggleClass('center', fitting === 'center'); + for (const option of ['cover', 'contain', 'stretch', 'center']) { + backgrounds.toggleClass(option, option === fitting); + } + background_settings.fitting = fitting; } function onBackgroundFilterInput() { From a03193b2f7ee553cffdaf68d61f115d3d422518c Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 27 Jan 2025 11:08:08 -0800 Subject: [PATCH 22/83] Change docker create to run so it actually runs the container. --- .github/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/readme.md b/.github/readme.md index 28d3a390d..01a3ab8ba 100644 --- a/.github/readme.md +++ b/.github/readme.md @@ -274,7 +274,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly 1. Open your Command Line 2. Run the following command -`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'` +`docker run --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'` > Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config. From 8b5e0df2d70b80d265a208cc021537c967e5a4d8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:56:15 +0200 Subject: [PATCH 23/83] Refactor reasoning placeholder clean-up --- public/script.js | 11 +++++++---- public/scripts/reasoning.js | 13 +++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index 38a4d562f..006f946d7 100644 --- a/public/script.js +++ b/public/script.js @@ -3194,10 +3194,6 @@ class StreamingProcessor { this.#checkDomElements(messageId); this.#updateMessageBlockVisibility(); const currentTime = new Date(); - // 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 timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount); chat[messageId]['mes'] = processedText; chat[messageId]['gen_started'] = this.timeStarted; chat[messageId]['gen_finished'] = currentTime; @@ -3214,6 +3210,10 @@ class StreamingProcessor { } } + // 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; + if (currentTokenCount) { chat[messageId]['extra']['token_count'] = currentTokenCount; if (this.messageTokenCounterDom instanceof HTMLElement) { @@ -3236,10 +3236,13 @@ class StreamingProcessor { if (this.messageTextDom instanceof HTMLElement) { this.messageTextDom.innerHTML = formattedText; } + + const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount); if (this.messageTimerDom instanceof HTMLElement) { this.messageTimerDom.textContent = timePassed.timerValue; this.messageTimerDom.title = timePassed.timerTitle; } + this.setFirstSwipe(messageId); } diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index 90721cd5d..c9b0efc3b 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -27,6 +27,7 @@ function getMessageFromJquery(element) { */ export class PromptReasoning { static REASONING_PLACEHOLDER = '\u200B'; + static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`); constructor() { this.counter = 0; @@ -121,8 +122,8 @@ function registerReasoningSlashCommands() { callback: (_args, value) => { const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1; const message = chat[messageId]; - const reasoning = message?.extra?.reasoning; - return reasoning !== PromptReasoning.REASONING_PLACEHOLDER ? reasoning : ''; + const reasoning = String(message?.extra?.reasoning ?? ''); + return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); }, })); @@ -151,7 +152,7 @@ function registerReasoningSlashCommands() { return ''; } - message.extra.reasoning = String(value); + message.extra.reasoning = String(value ?? ''); await saveChatConditional(); closeMessageEditor('reasoning'); @@ -181,12 +182,12 @@ function setReasoningEventHandlers(){ return; } - const reasoning = message?.extra?.reasoning; + const reasoning = String(message?.extra?.reasoning ?? ''); const chatElement = document.getElementById('chat'); const textarea = document.createElement('textarea'); const reasoningBlock = messageBlock.find('.mes_reasoning'); textarea.classList.add('reasoning_edit_textarea'); - textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; + textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); $(textarea).insertBefore(reasoningBlock); if (!CSS.supports('field-sizing', 'content')) { @@ -277,7 +278,7 @@ function setReasoningEventHandlers(){ $(document).on('pointerup', '.mes_reasoning_copy', async function () { const { message } = getMessageFromJquery(this); - const reasoning = message?.extra?.reasoning; + const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); if (!reasoning) { return; From abe240397d35d220820beac19d99d60580adb2ca Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:01:44 +0200 Subject: [PATCH 24/83] Set assistant role to bias in CC #3366 --- public/scripts/openai.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index d715377fb..2d0f44458 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name // Unordered prompts without marker { role: 'system', content: impersonationPrompt, identifier: 'impersonate' }, { role: 'system', content: quietPrompt, identifier: 'quietPrompt' }, - { role: 'system', content: bias, identifier: 'bias' }, { role: 'system', content: groupNudge, identifier: 'groupNudge' }, + { role: 'assistant', content: bias, identifier: 'bias' }, ]; // Tavern Extras - Summary From fad4e4e75e074dd99bca3d5adc63b946b3aa7023 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:30:35 +0200 Subject: [PATCH 25/83] Add command and profile for custom stop strings --- .../extensions/connection-manager/index.js | 4 +++ public/scripts/power-user.js | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index e9d0dd2fe..f31e4b495 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -30,6 +30,7 @@ const CC_COMMANDS = [ 'api-url', 'model', 'proxy', + 'stop-strings', ]; const TC_COMMANDS = [ @@ -43,6 +44,7 @@ const TC_COMMANDS = [ 'context', 'instruct-state', 'tokenizer', + 'stop-strings', ]; const FANCY_NAMES = { @@ -57,6 +59,7 @@ const FANCY_NAMES = { 'instruct': 'Instruct Template', 'context': 'Context Template', 'tokenizer': 'Tokenizer', + 'stop-strings': 'Custom Stopping Strings', }; /** @@ -138,6 +141,7 @@ const profilesProvider = () => [ * @property {string} [context] Context Template * @property {string} [instruct-state] Instruct Mode * @property {string} [tokenizer] Tokenizer + * @property {string} [stop-strings] Custom Stopping Strings * @property {string[]} [exclude] Commands to exclude */ diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 2a74f8722..2c1d5cd4c 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -4072,4 +4072,34 @@ $(document).ready(() => { ], helpString: 'activates a movingUI preset by name', })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'stop-strings', + aliases: ['stopping-strings'], + helpString: 'Sets a list of custom stopping strings. Gets the list if no value is provided.', + returns: ARGUMENT_TYPE.LIST, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'list of strings', + typeList: [ARGUMENT_TYPE.LIST], + acceptsMultiple: false, + isRequired: false, + }), + ], + callback: (_, value) => { + if (String(value ?? '').trim()) { + const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value); + if (!parsedValue || !Array.isArray(parsedValue)) { + throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.'); + } + parsedValue.forEach((item, index) => { + parsedValue[index] = String(item); + }); + power_user.custom_stopping_strings = JSON.stringify(parsedValue); + $('#custom_stopping_strings').val(power_user.custom_stopping_strings); + saveSettingsDebounced(); + } + + return power_user.custom_stopping_strings; + }, + })); }); From 6fc342d446f8bf6368852c0a2528c901a6e0a1b7 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:06:07 +0200 Subject: [PATCH 26/83] Add regex processing for reasoning blocks --- public/script.js | 59 +++++++++++++++------ public/scripts/extensions/regex/editor.html | 6 +++ public/scripts/extensions/regex/engine.js | 1 + 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/public/script.js b/public/script.js index 006f946d7..09d7a921b 100644 --- a/public/script.js +++ b/public/script.js @@ -1993,9 +1993,10 @@ export async function sendTextareaMessage() { * @param {boolean} isUser If the message was sent by the user * @param {number} messageId Message index in chat array * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides + * @param {boolean} [isReasoning] If the message is reasoning output * @returns {string} HTML string */ -export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) { +export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) { if (!mes) { return ''; } @@ -2029,6 +2030,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san if (!isSystem) { function getRegexPlacement() { try { + if (isReasoning) { + return regex_placement.REASONING; + } if (isUser) { return regex_placement.USER_INPUT; } else if (chat[messageId]?.extra?.type === 'narrator') { @@ -2250,8 +2254,8 @@ function getMessageFromTemplate({ export function updateMessageBlock(messageId, message) { const messageElement = $(`#chat [mesid="${messageId}"]`); const text = message?.extra?.display_text ?? message.mes; - messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId)); - messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1)); + 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)); addCopyToCodeBlocks(messageElement); appendMediaToMessage(message, messageElement); } @@ -2408,9 +2412,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll mes.is_user, chat.indexOf(mes), sanitizerOverrides, + false, ); - const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1); - const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1); + 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 params = { @@ -3205,7 +3210,7 @@ class StreamingProcessor { if (this.reasoning) { chat[messageId]['extra']['reasoning'] = this.reasoning; if (this.messageReasoningDom instanceof HTMLElement) { - const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1); + const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true); this.messageReasoningDom.innerHTML = formattedReasoning; } } @@ -3232,6 +3237,8 @@ class StreamingProcessor { chat[messageId].is_system, chat[messageId].is_user, messageId, + {}, + false, ); if (this.messageTextDom instanceof HTMLElement) { this.messageTextDom.innerHTML = formattedText; @@ -3383,7 +3390,7 @@ class StreamingProcessor { if (logprobs) { this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); } - this.reasoning = state?.reasoning ?? ''; + this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING); await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text)); } @@ -3850,14 +3857,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro coreChat.pop(); } - const reasoning = new PromptReasoning(); - for (let i = coreChat.length - 1; i >= 0; i--) { - if (reasoning.isLimitReached()) { - break; - } - coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) }; - } - coreChat = await Promise.all(coreChat.map(async (chatItem, index) => { let message = chatItem.mes; let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT; @@ -3877,6 +3876,25 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro }; })); + const reasoning = new PromptReasoning(); + for (let i = coreChat.length - 1; i >= 0; i--) { + if (reasoning.isLimitReached()) { + break; + } + const depth = coreChat.length - i - 1; + coreChat[i] = { + mes: reasoning.addToMessage( + coreChat[i].mes, + getRegexedString( + coreChat[i].extra?.reasoning, + regex_placement.REASONING, + { isPrompt: true, depth: depth }, + ), + ), + ...coreChat[i], + }; + } + // Determine token limit let this_max_context = getMaxContextSize(); @@ -4785,6 +4803,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro const swipes = extractMultiSwipes(data, type); messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); + reasoning = getRegexedString(reasoning, regex_placement.REASONING); if (isContinue) { getMessage = continue_mag + getMessage; @@ -7177,9 +7196,11 @@ function messageEditAuto(div) { mes.is_system, mes.is_user, this_edit_mes_id, + {}, + false, )); mesBlock.find('.mes_bias').empty(); - mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1)); + mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false)); saveChatDebounced(); } @@ -7201,10 +7222,12 @@ async function messageEditDone(div) { mes.is_system, mes.is_user, this_edit_mes_id, + {}, + false, ), ); mesBlock.find('.mes_bias').empty(); - mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1)); + mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false)); appendMediaToMessage(mes, div.closest('.mes')); addCopyToCodeBlocks(div.closest('.mes')); @@ -10841,6 +10864,8 @@ jQuery(async function () { chat[this_edit_mes_id].is_system, chat[this_edit_mes_id].is_user, this_edit_mes_id, + {}, + false, )); appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes')); addCopyToCodeBlocks($(this).closest('.mes')); diff --git a/public/scripts/extensions/regex/editor.html b/public/scripts/extensions/regex/editor.html index 9e699a622..8cfe9e008 100644 --- a/public/scripts/extensions/regex/editor.html +++ b/public/scripts/extensions/regex/editor.html @@ -94,6 +94,12 @@ World Info
+
+ +
diff --git a/public/scripts/extensions/regex/engine.js b/public/scripts/extensions/regex/engine.js index 8c28d01f3..08de07d6a 100644 --- a/public/scripts/extensions/regex/engine.js +++ b/public/scripts/extensions/regex/engine.js @@ -20,6 +20,7 @@ const regex_placement = { SLASH_COMMAND: 3, // 4 - sendAs (legacy) WORLD_INFO: 5, + REASONING: 6, }; export const substitute_find_regex = { From 0a413d63aa2630b755a6ce8df1411110e402eb4d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:11:31 +0200 Subject: [PATCH 27/83] Add reasoning regex on edit --- public/scripts/reasoning.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index c9b0efc3b..0bf7bee90 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -1,4 +1,5 @@ import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; +import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; import { t } from './i18n.js'; import { MacrosParser } from './macros.js'; import { Popup } from './popup.js'; @@ -224,7 +225,7 @@ function setReasoningEventHandlers(){ } const textarea = messageBlock.find('.reasoning_edit_textarea'); - const reasoning = String(textarea.val()); + const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true }); message.extra.reasoning = reasoning; await saveChatConditional(); updateMessageBlock(messageId, message); From 0fb9884ab8bcca2b43e2f2617117b197f5cda3f2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:59:25 +0200 Subject: [PATCH 28/83] Skill issue --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 09d7a921b..29abebbce 100644 --- a/public/script.js +++ b/public/script.js @@ -3883,6 +3883,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro } const depth = coreChat.length - i - 1; coreChat[i] = { + ...coreChat[i], mes: reasoning.addToMessage( coreChat[i].mes, getRegexedString( @@ -3891,7 +3892,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro { isPrompt: true, depth: depth }, ), ), - ...coreChat[i], }; } From d616dfef38a801114c0666ad3171ea8838777558 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 28 Jan 2025 00:03:38 +0200 Subject: [PATCH 29/83] Prevent log warning spam --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 29abebbce..ab94faf21 100644 --- a/public/script.js +++ b/public/script.js @@ -3887,7 +3887,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro mes: reasoning.addToMessage( coreChat[i].mes, getRegexedString( - coreChat[i].extra?.reasoning, + String(coreChat[i].extra?.reasoning ?? ''), regex_placement.REASONING, { isPrompt: true, depth: depth }, ), From 283ceb6bbfff486fdbbc8358c02099a4ff92a24d Mon Sep 17 00:00:00 2001 From: Dzmitry Kazlouski Date: Tue, 28 Jan 2025 02:51:05 +0300 Subject: [PATCH 30/83] Decrease TTS generation delay by splitting a message on a new line --- public/scripts/extensions/tts/index.js | 33 ++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 4b8978422..636103f23 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -120,7 +120,7 @@ async function onNarrateOneMessage() { } resetTtsPlayback(); - ttsJobQueue.push(message); + splitMessageAndAddToTtsJobQueue(message); moduleWorker(); } @@ -147,7 +147,7 @@ async function onNarrateText(args, text) { } resetTtsPlayback(); - ttsJobQueue.push({ mes: text, name: name }); + splitMessageAndAddToTtsJobQueue({ mes: text, name: name }); await moduleWorker(); // Return back to the chat voices @@ -220,6 +220,31 @@ function isTtsProcessing() { return processing; } +/** + * Splits a message into lines and adds each non-empty line to the TTS job queue. + * @param {Object} message - The message object to be processed. + * @param {string} message.mes - The text of the message to be split into lines. + * @param {string} message.name - The name associated with the message. + * @returns {void} + */ +function splitMessageAndAddToTtsJobQueue(message) { + const lines = message.mes.split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.length === 0) { + continue; + } + + ttsJobQueue.push( + Object.assign({}, message, { + mes: line, + }) + ); + } +} + function debugTtsPlayback() { console.log(JSON.stringify( { @@ -350,7 +375,7 @@ function onAudioControlClicked() { talkingAnimation(false); } else { // Default play behavior if not processing or playing is to play the last message. - ttsJobQueue.push(context.chat[context.chat.length - 1]); + splitMessageAndAddToTtsJobQueue(context.chat[context.chat.length - 1]); } updateUiAudioPlayState(); } @@ -816,7 +841,7 @@ async function onMessageEvent(messageId, lastCharIndex) { lastChatId = context.chatId; console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`); - ttsJobQueue.push(message); + splitMessageAndAddToTtsJobQueue(message); } async function onMessageDeleted() { From 145136059ea5c293f640cc1c4b2d6cc3017e6283 Mon Sep 17 00:00:00 2001 From: Small Eggs <144642298+small-eggs@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:43:24 -0800 Subject: [PATCH 31/83] Fix tts.skip_tags's regex to match newlines The extension_settings.tts.skip_tags setting is meant to skip sending tags and their content to the TTS API provider. The original regular expression matched content inside tags with ".*?". Unfortunately, Javascript's engine does *not* match newlines on the "." without the /s flag. The /s flag was added in ES2018. To be more compatible, the regex has been changed to "[\s\S]+?". This gives similar performance (instead of using capture groups) and matches all content within a tag, as the original regex intended. --- public/scripts/extensions/tts/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 4b8978422..279737f7a 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -466,7 +466,7 @@ async function processTtsQueue() { } if (extension_settings.tts.skip_tags) { - text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim(); + text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim(); } if (!extension_settings.tts.pass_asterisks) { From 63e7acb87bfdd1b10de1a2e669d371ae2ecd90f0 Mon Sep 17 00:00:00 2001 From: Dzmitry Kazlouski Date: Tue, 28 Jan 2025 05:50:25 +0300 Subject: [PATCH 32/83] Make this feature togglable in extensions > "Narrate by paragraphs (when not streaming)" --- public/scripts/extensions/tts/index.js | 27 +++++++++++++++++---- public/scripts/extensions/tts/settings.html | 4 +++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 636103f23..1d23807cb 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -120,7 +120,7 @@ async function onNarrateOneMessage() { } resetTtsPlayback(); - splitMessageAndAddToTtsJobQueue(message); + processAndQueueTtsMessage(message); moduleWorker(); } @@ -147,7 +147,7 @@ async function onNarrateText(args, text) { } resetTtsPlayback(); - splitMessageAndAddToTtsJobQueue({ mes: text, name: name }); + processAndQueueTtsMessage({ mes: text, name: name }); await moduleWorker(); // Return back to the chat voices @@ -227,7 +227,12 @@ function isTtsProcessing() { * @param {string} message.name - The name associated with the message. * @returns {void} */ -function splitMessageAndAddToTtsJobQueue(message) { +function processAndQueueTtsMessage(message) { + if (!extension_settings.tts.narrate_by_paragraphs) { + ttsJobQueue.push(message); + return; + } + const lines = message.mes.split("\n"); for (let i = 0; i < lines.length; i++) { @@ -375,7 +380,7 @@ function onAudioControlClicked() { talkingAnimation(false); } else { // Default play behavior if not processing or playing is to play the last message. - splitMessageAndAddToTtsJobQueue(context.chat[context.chat.length - 1]); + processAndQueueTtsMessage(context.chat[context.chat.length - 1]); } updateUiAudioPlayState(); } @@ -594,6 +599,7 @@ function loadSettings() { $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only); $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation); $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation); + $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs); $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only); $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user); $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks); @@ -663,6 +669,11 @@ function onPeriodicAutoGenerationClick() { saveSettingsDebounced(); } +function onNarrateByParagraphsClick() { + extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked'); + saveSettingsDebounced(); +} + function onNarrateDialoguesClick() { extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked'); @@ -841,7 +852,12 @@ async function onMessageEvent(messageId, lastCharIndex) { lastChatId = context.chatId; console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`); - splitMessageAndAddToTtsJobQueue(message); + + if (extension_settings.tts.periodic_auto_generation) { + ttsJobQueue.push(message); + } else { + processAndQueueTtsMessage(message); + } } async function onMessageDeleted() { @@ -1181,6 +1197,7 @@ jQuery(async function () { $('#tts_pass_asterisks').on('click', onPassAsterisksClick); $('#tts_auto_generation').on('click', onAutoGenerationClick); $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick); + $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick); $('#tts_narrate_user').on('click', onNarrateUserClick); $('#playback_rate').on('input', function () { diff --git a/public/scripts/extensions/tts/settings.html b/public/scripts/extensions/tts/settings.html index dc005a406..5fb5b6895 100644 --- a/public/scripts/extensions/tts/settings.html +++ b/public/scripts/extensions/tts/settings.html @@ -30,6 +30,10 @@ Narrate by paragraphs (when streaming) +