`refactor/perf-printMessages` #3: Extracted `updateMessageItemizedPromptButton` and `getMessageHTML` from `addOneMessage` to improve readability. (#4984) * Extracted updateMessageItemizedPromptButton and getMessageHTML from addOneMessage to improve readability. * Fix types and function calls * Fixed insertBefore, insertAfter and messages without extra. * Use strict comparison operator * Use logical OR for display text fallback * Fixed newMessageId again. * Faster showMoreMessages. * removed `insertAfter` and `insertBefore` from `mes_edit_copy`, `/message-role` and `/message-name`. * Formatting fix * Refactor newMessageId --------- Co-authored-by: user <user@exmaple.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -1391,18 +1391,26 @@ export async function showMoreMessages(messagesToLoad = null) { | |||
| 1391 | 1391 | ||
| 1392 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); | 1392 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); |
| 1393 | const prevHeight = chatElement.prop('scrollHeight'); | 1393 | const prevHeight = chatElement.prop('scrollHeight'); |
| 1394 | const isButtonInView = isElementInViewport($('#show_more_messages')[0]); | 1394 | const showMoreButton = $('#show_more_messages'); |
| 1395 | 1395 | const isButtonInView = isElementInViewport(showMoreButton[0]); | |
| 1396 | while (messageId > 0 && count > 0) { | 1396 | |
| 1397 | let newMessageId = messageId - 1; | 1397 | const firstId = clamp(messageId - count, 0, Infinity); |
| 1398 | addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId, showSwipes: false }); | 1398 | const messageElements = []; |
| 1399 | count--; | 1399 | chat.slice(firstId, messageId).forEach((message, id) => { |
| 1400 | messageId--; | 1400 | messageElements.push(addOneMessage(message, { scroll: false, forceId: firstId + id, showSwipes: false, insert: false })); |
| 1401 | }); | ||
| 1402 | // This could be faster: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement | ||
| 1403 | // Fallback to chatElement if the button isn't where it's expected to be. | ||
| 1404 | if (showMoreButton[0]) { | ||
| 1405 | showMoreButton.after(messageElements); | ||
| 1406 | } else { | ||
| 1407 | chatElement.prepend(messageElements); | ||
| 1401 | } | 1408 | } |
| 1409 | |||
| 1402 | refreshSwipeButtons(); | 1410 | refreshSwipeButtons(); |
| 1403 | 1411 | ||
| 1404 | if (messageId == 0) { | 1412 | if (firstId === 0) { |
| 1405 | $('#show_more_messages').remove(); | 1413 | showMoreButton.remove(); |
| 1406 | } | 1414 | } |
| 1407 | 1415 | ||
| 1408 | if (isButtonInView) { | 1416 | if (isButtonInView) { |
| @@ -1682,7 +1690,7 @@ export async function sendTextareaMessage() { | |||
| 1682 | * @param {boolean} isSystem If the message was sent by the system | 1690 | * @param {boolean} isSystem If the message was sent by the system |
| 1683 | * @param {boolean} isUser If the message was sent by the user | 1691 | * @param {boolean} isUser If the message was sent by the user |
| 1684 | * @param {number} messageId Message index in chat array | 1692 | * @param {number} messageId Message index in chat array |
| 1685 | * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides | 1693 | * @param {Partial<DOMPurify.Config>} [sanitizerOverrides] DOMPurify sanitizer option overrides |
| 1686 | * @param {boolean} [isReasoning] If the message is reasoning output | 1694 | * @param {boolean} [isReasoning] If the message is reasoning output |
| 1687 | * @returns {string} HTML string | 1695 | * @returns {string} HTML string |
| 1688 | */ | 1696 | */ |
| @@ -1831,7 +1839,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | |||
| 1831 | mes = mes.replace(new RegExp(`(^|\n)${escapeRegex(ch_name)}:`, 'g'), '$1'); | 1839 | mes = mes.replace(new RegExp(`(^|\n)${escapeRegex(ch_name)}:`, 'g'), '$1'); |
| 1832 | } | 1840 | } |
| 1833 | 1841 | ||
| 1834 | /** @type {import('dompurify').Config & { RETURN_DOM_FRAGMENT: false; RETURN_DOM: false }} */ | 1842 | /** @type {DOMPurify.Config} */ |
| 1835 | const config = { | 1843 | const config = { |
| 1836 | RETURN_DOM: false, | 1844 | RETURN_DOM: false, |
| 1837 | RETURN_DOM_FRAGMENT: false, | 1845 | RETURN_DOM_FRAGMENT: false, |
| @@ -2375,6 +2383,46 @@ export function addCopyToCodeBlocks(messageElement) { | |||
| 2375 | } | 2383 | } |
| 2376 | } | 2384 | } |
| 2377 | 2385 | ||
| 2386 | /** | ||
| 2387 | * Shows or hides the Prompt display button | ||
| 2388 | * @param {ChatMessage} message Message object | ||
| 2389 | * @param {object} options Options | ||
| 2390 | * @param {number} [options.messageId] Message ID | ||
| 2391 | * @param {JQuery<HTMLElement>} [options.messageElement] Message element | ||
| 2392 | * @return {void} | ||
| 2393 | */ | ||
| 2394 | function updateMessageItemizedPromptButton(message, { messageId = chat.indexOf(message), messageElement = chatElement.find(`.mes[mesid="${messageId}"]`) }) { | ||
| 2395 | //if we have itemized messages, and the array isn't null.. | ||
| 2396 | if (!message.is_user && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) { | ||
| 2397 | const itemizedPrompt = itemizedPrompts.find(x => Number(x.mesId) === Number(messageId)); | ||
| 2398 | if (itemizedPrompt) { | ||
| 2399 | messageElement.find('.mes_prompt').show(); | ||
| 2400 | } | ||
| 2401 | } | ||
| 2402 | } | ||
| 2403 | |||
| 2404 | /** | ||
| 2405 | * Gets messageFormatting for a ChatMessage object. | ||
| 2406 | * @param {ChatMessage} message | ||
| 2407 | * @param {object} options Options | ||
| 2408 | * @param {number} [options.messageId] Message ID | ||
| 2409 | * @returns {string} Formatted message HTML | ||
| 2410 | */ | ||
| 2411 | function getMessageTextHTML(message, { messageId = chat.indexOf(message) }) { | ||
| 2412 | // if mes.extra.uses_system_ui is true, set an override on the sanitizer options | ||
| 2413 | /** @type {Partial<DOMPurify.Config>} */ | ||
| 2414 | const sanitizerOverrides = message.extra?.uses_system_ui ? { MESSAGE_ALLOW_SYSTEM_UI: true } : {}; | ||
| 2415 | |||
| 2416 | return messageFormatting( | ||
| 2417 | message.extra?.display_text || message.mes, | ||
| 2418 | message.name, | ||
| 2419 | message.is_system, | ||
| 2420 | message.is_user, | ||
| 2421 | messageId, | ||
| 2422 | sanitizerOverrides, | ||
| 2423 | false, | ||
| 2424 | ); | ||
| 2425 | } | ||
| 2378 | 2426 | ||
| 2379 | /** | 2427 | /** |
| 2380 | * Adds a single message to the chat. | 2428 | * Adds a single message to the chat. |
| @@ -2391,18 +2439,27 @@ export function addCopyToCodeBlocks(messageElement) { | |||
| 2391 | */ | 2439 | */ |
| 2392 | export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true, insert = true } = {}) { | 2440 | export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true, insert = true } = {}) { |
| 2393 | // Callers push the new message to chat before calling addOneMessage | 2441 | // Callers push the new message to chat before calling addOneMessage |
| 2394 | const newMessageId = typeof forceId == 'number' ? forceId : chat.length - 1; | 2442 | const newMessageId = (() => { |
| 2443 | if (typeof forceId === 'number') { | ||
| 2444 | return forceId; | ||
| 2445 | } | ||
| 2446 | if (typeof insertBefore === 'number') { | ||
| 2447 | return insertBefore - 1; | ||
| 2448 | } | ||
| 2449 | if (typeof insertAfter === 'number') { | ||
| 2450 | return insertAfter + 1; | ||
| 2451 | } | ||
| 2452 | const index = chat.indexOf(mes); | ||
| 2453 | if (index !== -1) { | ||
| 2454 | return index; | ||
| 2455 | } | ||
| 2456 | return chat.length - 1; | ||
| 2457 | })(); | ||
| 2395 | 2458 | ||
| 2396 | let messageText = mes.mes; | ||
| 2397 | const momentDate = timestampToMoment(mes.send_date); | 2459 | const momentDate = timestampToMoment(mes.send_date); |
| 2398 | const timestamp = momentDate.isValid() ? momentDate.format('LL LT') : ''; | 2460 | const timestamp = momentDate.isValid() ? momentDate.format('LL LT') : ''; |
| 2399 | 2461 | ||
| 2400 | if (mes?.extra?.display_text) { | ||
| 2401 | messageText = mes.extra.display_text; | ||
| 2402 | } | ||
| 2403 | |||
| 2404 | let avatarImg = getThumbnailUrl('persona', user_avatar); | 2462 | let avatarImg = getThumbnailUrl('persona', user_avatar); |
| 2405 | const isSystem = mes.is_system; | ||
| 2406 | 2463 | ||
| 2407 | //for non-user messages | 2464 | //for non-user messages |
| 2408 | if (!mes.is_user) { | 2465 | if (!mes.is_user) { |
| @@ -2426,18 +2483,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | |||
| 2426 | avatarImg = mes.force_avatar; | 2483 | avatarImg = mes.force_avatar; |
| 2427 | } | 2484 | } |
| 2428 | 2485 | ||
| 2429 | // if mes.extra.uses_system_ui is true, set an override on the sanitizer options | 2486 | const messageHTML = getMessageTextHTML(mes, { messageId: newMessageId }); |
| 2430 | const sanitizerOverrides = mes.extra?.uses_system_ui ? { MESSAGE_ALLOW_SYSTEM_UI: true } : {}; | ||
| 2431 | |||
| 2432 | messageText = messageFormatting( | ||
| 2433 | messageText, | ||
| 2434 | mes.name, | ||
| 2435 | isSystem, | ||
| 2436 | mes.is_user, | ||
| 2437 | chat.indexOf(mes), | ||
| 2438 | sanitizerOverrides, | ||
| 2439 | false, | ||
| 2440 | ); | ||
| 2441 | let newMessage; | 2487 | let newMessage; |
| 2442 | 2488 | ||
| 2443 | if (type === 'swipe') { | 2489 | if (type === 'swipe') { |
| @@ -2488,7 +2534,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | |||
| 2488 | insertSVGIcon(newMessage, mes.extra); | 2534 | insertSVGIcon(newMessage, mes.extra); |
| 2489 | } | 2535 | } |
| 2490 | 2536 | ||
| 2491 | |||
| 2492 | if (type !== 'swipe' && insert) { | 2537 | if (type !== 'swipe' && insert) { |
| 2493 | if (!insertAfter && !insertBefore) { | 2538 | if (!insertAfter && !insertBefore) { |
| 2494 | chatElement.append(newMessage); | 2539 | chatElement.append(newMessage); |
| @@ -2512,13 +2557,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | |||
| 2512 | newMessage.addClass('toolCall'); | 2557 | newMessage.addClass('toolCall'); |
| 2513 | } | 2558 | } |
| 2514 | 2559 | ||
| 2515 | //if we have itemized messages, and the array isn't null.. | 2560 | updateMessageItemizedPromptButton(mes, { messageId: newMessageId, messageElement: newMessage }); |
| 2516 | if (!mes.is_user && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) { | ||
| 2517 | const itemizedPrompt = itemizedPrompts.find(x => Number(x.mesId) === Number(newMessageId)); | ||
| 2518 | if (itemizedPrompt) { | ||
| 2519 | newMessage.find('.mes_prompt').show(); | ||
| 2520 | } | ||
| 2521 | } | ||
| 2522 | 2561 | ||
| 2523 | newMessage.find('.avatar img').on('error', function () { | 2562 | newMessage.find('.avatar img').on('error', function () { |
| 2524 | $(this).hide(); | 2563 | $(this).hide(); |
| @@ -2526,8 +2565,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | |||
| 2526 | }); | 2565 | }); |
| 2527 | 2566 | ||
| 2528 | appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE); | 2567 | appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE); |
| 2529 | newMessage.find('.mes_text').html(messageText); | 2568 | newMessage.find('.mes_text').html(messageHTML); |
| 2530 | |||
| 2531 | addCopyToCodeBlocks(newMessage); | 2569 | addCopyToCodeBlocks(newMessage); |
| 2532 | 2570 | ||
| 2533 | // Set the swipes counter for all non-user messages. | 2571 | // Set the swipes counter for all non-user messages. |
| @@ -11651,14 +11689,16 @@ jQuery(async function () { | |||
| 11651 | const oldScroll = chatElement[0].scrollTop; | 11689 | const oldScroll = chatElement[0].scrollTop; |
| 11652 | const clone = structuredClone(chat[this_edit_mes_id]); | 11690 | const clone = structuredClone(chat[this_edit_mes_id]); |
| 11653 | clone.send_date = Date.now(); | 11691 | clone.send_date = Date.now(); |
| 11654 | clone.mes = $(this).closest('.mes').find('.edit_textarea').val().toString(); | 11692 | const this_edit_mes_element = $(this).closest('.mes'); |
| 11693 | clone.mes = this_edit_mes_element.find('.edit_textarea').val().toString(); | ||
| 11655 | 11694 | ||
| 11656 | if (power_user.trim_spaces) { | 11695 | if (power_user.trim_spaces) { |
| 11657 | clone.mes = clone.mes.trim(); | 11696 | clone.mes = clone.mes.trim(); |
| 11658 | } | 11697 | } |
| 11659 | 11698 | ||
| 11660 | chat.splice(Number(this_edit_mes_id) + 1, 0, clone); | 11699 | chat.splice(Number(this_edit_mes_id) + 1, 0, clone); |
| 11661 | addOneMessage(clone, { insertAfter: this_edit_mes_id }); | 11700 | const newMessageElement = addOneMessage(clone, { insert: false }); |
| 11701 | this_edit_mes_element.after(newMessageElement); | ||
| 11662 | 11702 | ||
| 11663 | updateViewMessageIds(); | 11703 | updateViewMessageIds(); |
| 11664 | await saveChatConditional(); | 11704 | await saveChatConditional(); |
| @@ -685,7 +685,7 @@ export function formatCreatorNotes(text, avatarId) { | |||
| 685 | const preference = new StylesPreference(avatarId); | 685 | const preference = new StylesPreference(avatarId); |
| 686 | const sanitizeStyles = !preference.get(); | 686 | const sanitizeStyles = !preference.get(); |
| 687 | const decodeStyleParam = { prefix: sanitizeStyles ? '#creator_notes_spoiler ' : '' }; | 687 | const decodeStyleParam = { prefix: sanitizeStyles ? '#creator_notes_spoiler ' : '' }; |
| 688 | /** @type {import('dompurify').Config} */ | 688 | /** @type {DOMPurify.Config} */ |
| 689 | const config = { | 689 | const config = { |
| 690 | RETURN_DOM: false, | 690 | RETURN_DOM: false, |
| 691 | RETURN_DOM_FRAGMENT: false, | 691 | RETURN_DOM_FRAGMENT: false, |
| @@ -4642,7 +4642,8 @@ async function messageRoleCallback(args, role) { | |||
| 4642 | await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt); | 4642 | await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt); |
| 4643 | const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`); | 4643 | const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`); |
| 4644 | if (existingMessage.length) { | 4644 | if (existingMessage.length) { |
| 4645 | addOneMessage(message, { forceId: modifyAt, insertAfter: modifyAt, scroll: false }); | 4645 | const newMessageElement = addOneMessage(message, { forceId: modifyAt, insert: false, scroll: false }); |
| 4646 | existingMessage.after(newMessageElement); | ||
| 4646 | existingMessage.remove(); | 4647 | existingMessage.remove(); |
| 4647 | } | 4648 | } |
| 4648 | await eventSource.emit(event_types.MESSAGE_UPDATED, modifyAt); | 4649 | await eventSource.emit(event_types.MESSAGE_UPDATED, modifyAt); |
| @@ -4707,7 +4708,8 @@ async function messageNameCallback(args, name) { | |||
| 4707 | await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt); | 4708 | await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt); |
| 4708 | const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`); | 4709 | const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`); |
| 4709 | if (existingMessage.length) { | 4710 | if (existingMessage.length) { |
| 4710 | addOneMessage(message, { forceId: modifyAt, insertAfter: modifyAt, scroll: false }); | 4711 | const newMessageElement = addOneMessage(message, { forceId: modifyAt, insert: false, scroll: false }); |
| 4712 | existingMessage.after(newMessageElement); | ||
| 4711 | existingMessage.remove(); | 4713 | existingMessage.remove(); |
| 4712 | } | 4714 | } |
| 4713 | await eventSource.emit(event_types.MESSAGE_UPDATED, modifyAt); | 4715 | await eventSource.emit(event_types.MESSAGE_UPDATED, modifyAt); |