Added translation to reasoning block (#3617) * Added translate to reasoning block * Added mising reset value * Shortcut nullable type * Added reasoning edited/deleted events, better naming * Fixed async call * Added await to saveChat calls * Exported updateReasoningUI * Removed translated reasoning on edit if auto mode is none * Added new value check before updating reasoning block, fixed an issue that display value stays same when we edit the message. * Translate reasoning before the main message * Fixed auto mode translate for reasoning message * Translate reasoning first. Prevent out of bounds access * Fix translating reasoning on swipe generation --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -453,6 +453,8 @@ export const event_types = { | ||
| 453 | 453 | MESSAGE_DELETED: 'message_deleted', |
| 454 | 454 | MESSAGE_UPDATED: 'message_updated', |
| 455 | 455 | MESSAGE_FILE_EMBEDDED: 'message_file_embedded', |
| 456 | + MESSAGE_REASONING_EDITED: 'message_reasoning_edited', | |
| 457 | + MESSAGE_REASONING_DELETED: 'message_reasoning_deleted', | |
| 456 | 458 | MORE_MESSAGES_LOADED: 'more_messages_loaded', |
| 457 | 459 | IMPERSONATE_READY: 'impersonate_ready', |
| 458 | 460 | CHAT_CHANGED: 'chat_id_changed', |
| @@ -11,6 +11,7 @@ import { | ||
| 11 | 11 | } from '../../../script.js'; |
| 12 | 12 | import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 13 | 13 | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js'; |
| 14 | +import { updateReasoningUI } from '../../reasoning.js'; | |
| 14 | 15 | import { findSecret, secret_state, writeSecret } from '../../secrets.js'; |
| 15 | 16 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 16 | 17 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| @@ -172,21 +173,38 @@ function loadSettings() { | ||
| 172 | 173 | showKeysButton(); |
| 173 | 174 | } |
| 174 | 175 | |
| 176 | +/** | |
| 177 | + * Check if the swipe is being generated for a message. | |
| 178 | + * @param {string|number} messageId Message ID | |
| 179 | + * @returns {boolean} Whether the swipe is being generated | |
| 180 | + */ | |
| 181 | +function isGeneratingSwipe(messageId) { | |
| 182 | + return $(`#chat .mes[mesid="${messageId}"] .mes_text`).text() === '...'; | |
| 183 | +} | |
| 184 | + | |
| 175 | 185 | async function translateImpersonate(text) { |
| 176 | 186 | const translatedText = await translate(text, extension_settings.translate.target_language); |
| 177 | 187 | $('#send_textarea').val(translatedText); |
| 178 | 188 | } |
| 179 | 189 | |
| 190 | +/** | |
| 191 | + * Translates the contents of an incoming message. | |
| 192 | + * @param {string | number} messageId Message ID | |
| 193 | + * @returns {Promise<void>} | |
| 194 | + */ | |
| 180 | 195 | async function translateIncomingMessage(messageId) { |
| 181 | 196 | const context = getContext(); |
| 182 | 197 | const message = context.chat[messageId]; |
| 183 | 198 | |
| 199 | + if (!message) { | |
| 200 | + return; | |
| 201 | + } | |
| 202 | + | |
| 184 | 203 | if (typeof message.extra !== 'object') { |
| 185 | 204 | message.extra = {}; |
| 186 | 205 | } |
| 187 | 206 | |
| 188 | - // New swipe is being generated. Don't translate that | |
| 207 | + if (isGeneratingSwipe(messageId)) { | |
| 189 | - if ($(`#chat .mes[mesid="${messageId}"] .mes_text`).text() == '...') { | |
| 190 | 208 | return; |
| 191 | 209 | } |
| 192 | 210 | |
| @@ -194,7 +212,36 @@ async function translateIncomingMessage(messageId) { | ||
| 194 | 212 | const translation = await translate(textToTranslate, extension_settings.translate.target_language); |
| 195 | 213 | message.extra.display_text = translation; |
| 196 | 214 | |
| 197 | 215 | updateMessageBlock(Number(messageId), message); |
| 216 | +} | |
| 217 | + | |
| 218 | +/** | |
| 219 | + * Translates the reasoning of an incoming message. | |
| 220 | + * @param {string | number} messageId | |
| 221 | + * @returns {Promise<boolean>} translated or not | |
| 222 | + */ | |
| 223 | +async function translateIncomingMessageReasoning(messageId) { | |
| 224 | + const context = getContext(); | |
| 225 | + const message = context.chat[messageId]; | |
| 226 | + | |
| 227 | + if (!message) { | |
| 228 | + return false; | |
| 229 | + } | |
| 230 | + | |
| 231 | + if (typeof message.extra !== 'object') { | |
| 232 | + message.extra = {}; | |
| 233 | + } | |
| 234 | + | |
| 235 | + if (!message.extra.reasoning || isGeneratingSwipe(messageId)) { | |
| 236 | + return false; | |
| 237 | + } | |
| 238 | + | |
| 239 | + const textToTranslate = substituteParams(message.extra.reasoning, context.name1, message.name); | |
| 240 | + const translation = await translate(textToTranslate, extension_settings.translate.target_language); | |
| 241 | + message.extra.reasoning_display_text = translation; | |
| 242 | + | |
| 243 | + updateReasoningUI(Number(messageId)); | |
| 244 | + return true; | |
| 198 | 245 | } |
| 199 | 246 | |
| 200 | 247 | async function translateProviderOneRing(text, lang) { |
| @@ -535,6 +582,7 @@ async function onTranslateChatClick() { | ||
| 535 | 582 | toastr.info(`${chat.length} message(s) queued for translation.`, 'Please wait...'); |
| 536 | 583 | |
| 537 | 584 | for (let i = 0; i < chat.length; i++) { |
| 585 | + await translateIncomingMessageReasoning(i); | |
| 538 | 586 | await translateIncomingMessage(i); |
| 539 | 587 | } |
| 540 | 588 | |
| @@ -561,6 +609,7 @@ async function onTranslationsClearClick() { | ||
| 561 | 609 | for (const mes of chat) { |
| 562 | 610 | if (mes.extra) { |
| 563 | 611 | delete mes.extra.display_text; |
| 612 | + delete mes.extra.reasoning_display_text; | |
| 564 | 613 | } |
| 565 | 614 | } |
| 566 | 615 | |
| @@ -573,12 +622,47 @@ async function translateMessageEdit(messageId) { | ||
| 573 | 622 | const chat = context.chat; |
| 574 | 623 | const message = chat[messageId]; |
| 575 | 624 | |
| 576 | - if (message.is_system || extension_settings.translate.auto_mode == autoModeOptions.NONE) { | |
| 625 | + let anyChange = false; | |
| 577 | - return; | |
| 626 | + if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.display_text)) { | |
| 627 | + delete message.extra.display_text; | |
| 628 | + updateMessageBlock(messageId, message); | |
| 629 | + anyChange = true; | |
| 630 | + } else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) { | |
| 631 | + await translateIncomingMessage(messageId); | |
| 632 | + anyChange = true; | |
| 578 | 633 | } |
| 579 | 634 | |
| 580 | - if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) { | |
| 635 | + if (anyChange) { | |
| 581 | 636 | await translateIncomingMessagecontext.saveChat(messageId); |
| 637 | + } | |
| 638 | +} | |
| 639 | + | |
| 640 | +async function translateMessageReasoningEdit(messageId) { | |
| 641 | + const context = getContext(); | |
| 642 | + const chat = context.chat; | |
| 643 | + const message = chat[messageId]; | |
| 644 | + | |
| 645 | + let anyChange = false; | |
| 646 | + if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.reasoning_display_text)) { | |
| 647 | + delete message.extra.reasoning_display_text; | |
| 648 | + updateReasoningUI(Number(messageId)); | |
| 649 | + anyChange = true; | |
| 650 | + } else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) { | |
| 651 | + anyChange = await translateIncomingMessageReasoning(messageId); | |
| 652 | + } | |
| 653 | + | |
| 654 | + if (anyChange) { | |
| 655 | + await context.saveChat(); | |
| 656 | + } | |
| 657 | +} | |
| 658 | + | |
| 659 | +async function removeReasoningDisplayText(messageId) { | |
| 660 | + const context = getContext(); | |
| 661 | + const message = context.chat[messageId]; | |
| 662 | + if (message.extra?.reasoning_display_text) { | |
| 663 | + delete message.extra.reasoning_display_text; | |
| 664 | + updateReasoningUI(Number(messageId)); | |
| 665 | + await context.saveChat(); | |
| 582 | 666 | } |
| 583 | 667 | } |
| 584 | 668 | |
| @@ -588,22 +672,36 @@ async function onMessageTranslateClick() { | ||
| 588 | 672 | const message = context.chat[messageId]; |
| 589 | 673 | |
| 590 | 674 | // If the message is already translated, revert it back to the original text |
| 675 | + let alreadyTranslated = false; | |
| 591 | 676 | if (message?.extra?.display_text) { |
| 592 | 677 | delete message.extra.display_text; |
| 593 | 678 | updateMessageBlock(Number(messageId), message); |
| 679 | + alreadyTranslated = true; | |
| 594 | 680 | } |
| 681 | + if (message?.extra?.reasoning_display_text) { | |
| 682 | + delete message.extra.reasoning_display_text; | |
| 683 | + updateReasoningUI(Number(messageId)); | |
| 684 | + alreadyTranslated = true; | |
| 685 | + } | |
| 686 | + | |
| 595 | 687 | // If the message is not translated, translate it |
| 596 | - else { | |
| 688 | + if (!alreadyTranslated) { | |
| 689 | + await translateIncomingMessageReasoning(messageId); | |
| 597 | 690 | await translateIncomingMessage(messageId); |
| 598 | 691 | } |
| 599 | 692 | |
| 600 | 693 | await context.saveChat(); |
| 601 | 694 | } |
| 602 | 695 | |
| 603 | 696 | const handleIncomingMessage = createEventHandler(translateIncomingMessage,async (messageId) => shouldTranslate(incomingTypes));{ |
| 697 | + await translateIncomingMessageReasoning(messageId); | |
| 698 | + await translateIncomingMessage(messageId); | |
| 699 | +}, () => shouldTranslate(incomingTypes)); | |
| 604 | 700 | const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => shouldTranslate(outgoingTypes)); |
| 605 | 701 | const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes)); |
| 606 | 702 | const handleMessageEdit = createEventHandler(translateMessageEdit, () => true); |
| 703 | +const handleMessageReasoningEdit = createEventHandler(translateMessageReasoningEdit, () => true); | |
| 704 | +const handleMessageReasoningDelete = createEventHandler(removeReasoningDisplayText, () => true); | |
| 607 | 705 | |
| 608 | 706 | globalThis.translate = translate; |
| 609 | 707 | |
| @@ -717,6 +815,8 @@ jQuery(async () => { | ||
| 717 | 815 | eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage); |
| 718 | 816 | eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady); |
| 719 | 817 | eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit); |
| 818 | + eventSource.on(event_types.MESSAGE_REASONING_EDITED, handleMessageReasoningEdit); | |
| 819 | + eventSource.on(event_types.MESSAGE_REASONING_DELETED, handleMessageReasoningDelete); | |
| 720 | 820 | |
| 721 | 821 | document.body.classList.add('translate'); |
| 722 | 822 | |
| @@ -167,6 +167,8 @@ export class ReasoningHandler { | ||
| 167 | 167 | this.type = null; |
| 168 | 168 | /** @type {string} The reasoning output */ |
| 169 | 169 | this.reasoning = ''; |
| 170 | + /** @type {string?} The reasoning output display in case of translate or other */ | |
| 171 | + this.reasoningDisplayText = null; | |
| 170 | 172 | /** @type {Date} When the reasoning started */ |
| 171 | 173 | this.startTime = null; |
| 172 | 174 | /** @type {Date} When the reasoning ended */ |
| @@ -234,6 +236,7 @@ export class ReasoningHandler { | ||
| 234 | 236 | |
| 235 | 237 | this.type = extra?.reasoning_type; |
| 236 | 238 | this.reasoning = extra?.reasoning ?? ''; |
| 239 | + this.reasoningDisplayText = extra?.reasoning_display_text ?? null; | |
| 237 | 240 | |
| 238 | 241 | if (this.state !== ReasoningState.None) { |
| 239 | 242 | this.initialTime = new Date(chat[messageId].gen_started); |
| @@ -249,6 +252,7 @@ export class ReasoningHandler { | ||
| 249 | 252 | this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None; |
| 250 | 253 | this.type = null; |
| 251 | 254 | this.reasoning = ''; |
| 255 | + this.reasoningDisplayText = null; | |
| 252 | 256 | this.initialTime = new Date(); |
| 253 | 257 | this.startTime = null; |
| 254 | 258 | this.endTime = null; |
| @@ -434,7 +438,7 @@ export class ReasoningHandler { | ||
| 434 | 438 | setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type); |
| 435 | 439 | |
| 436 | 440 | // Update the reasoning message |
| 437 | 441 | const reasoning = trimSpaces(this.reasoningDisplayText ?? this.reasoning); |
| 438 | 442 | const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); |
| 439 | 443 | this.messageReasoningContentDom.innerHTML = displayReasoning; |
| 440 | 444 | |
| @@ -888,12 +892,17 @@ function setReasoningEventHandlers() { | ||
| 888 | 892 | } |
| 889 | 893 | |
| 890 | 894 | const textarea = messageBlock.find('.reasoning_edit_textarea'); |
| 891 | 895 | updateReasoningFromValue(message,const newReasoning = String(textarea.val())); |
| 896 | + textarea.remove(); | |
| 897 | + if (newReasoning === message.extra.reasoning) { | |
| 898 | + return; | |
| 899 | + } | |
| 900 | + updateReasoningFromValue(message, newReasoning); | |
| 892 | 901 | await saveChatConditional(); |
| 893 | 902 | updateMessageBlock(messageId, message); |
| 894 | - textarea.remove(); | |
| 895 | 903 | |
| 896 | 904 | messageBlock.find('.mes_edit_done:visible').trigger('click'); |
| 905 | + await eventSource.emit(event_types.MESSAGE_REASONING_EDITED, messageId); | |
| 897 | 906 | }); |
| 898 | 907 | |
| 899 | 908 | $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { |
| @@ -955,6 +964,7 @@ function setReasoningEventHandlers() { | ||
| 955 | 964 | updateMessageBlock(messageId, message); |
| 956 | 965 | const textarea = messageBlock.find('.reasoning_edit_textarea'); |
| 957 | 966 | textarea.remove(); |
| 967 | + await eventSource.emit(event_types.MESSAGE_REASONING_DELETED, messageId); | |
| 958 | 968 | }); |
| 959 | 969 | |
| 960 | 970 | $(document).on('pointerup', '.mes_reasoning_copy', async function () { |
| @@ -79,6 +79,7 @@ import { timestampToMoment, uuidv4 } from './utils.js'; | ||
| 79 | 79 | import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js'; |
| 80 | 80 | import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js'; |
| 81 | 81 | import { ChatCompletionService, TextCompletionService } from './custom-request.js'; |
| 82 | +import { updateReasoningUI } from './reasoning.js'; | |
| 82 | 83 | |
| 83 | 84 | export function getContext() { |
| 84 | 85 | return { |
| @@ -211,6 +212,7 @@ export function getContext() { | ||
| 211 | 212 | clearChat, |
| 212 | 213 | ChatCompletionService, |
| 213 | 214 | TextCompletionService, |
| 215 | + updateReasoningUI, | |
| 214 | 216 | unshallowCharacter, |
| 215 | 217 | unshallowGroupMembers, |
| 216 | 218 | }; |