`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 | 1392 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); |
| 1393 | 1393 | const prevHeight = chatElement.prop('scrollHeight'); |
| 1394 | 1394 | const isButtonInViewshowMoreButton = isElementInViewport($('#show_more_messages')[0]); |
| 1395 | - | |
| 1395 | + const isButtonInView = isElementInViewport(showMoreButton[0]); | |
| 1396 | - while (messageId > 0 && count > 0) { | |
| 1396 | + | |
| 1397 | 1397 | let const newMessageIdfirstId = clamp(messageId - 1count, 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 | 1410 | refreshSwipeButtons(); |
| 1403 | 1411 | |
| 1404 | 1412 | if (messageIdfirstId === 0) { |
| 1405 | 1413 | $('#show_more_messages')showMoreButton.remove(); |
| 1406 | 1414 | } |
| 1407 | 1415 | |
| 1408 | 1416 | if (isButtonInView) { |
| @@ -1448,7 +1456,7 @@ export async function redisplayChat({ targetChat = chat, startIndex = 0, fade = | ||
| 1448 | 1456 | const messages = targetChat.slice(startIndex); |
| 1449 | 1457 | |
| 1450 | 1458 | if (messages.length > 0) { |
| 1451 | 1459 | const newMessageElements = messages.map( (message, offset) => { |
| 1452 | 1460 | const i = startIndex + offset; |
| 1453 | 1461 | const messageElement = addOneMessage(message, { scroll: false, forceId: i, showSwipes: false, insert: false }); |
| 1454 | 1462 | |
| @@ -1461,7 +1469,7 @@ export async function redisplayChat({ targetChat = chat, startIndex = 0, fade = | ||
| 1461 | 1469 | //Append to chat in one DOM update. |
| 1462 | 1470 | chatElement.append(newMessageElements); |
| 1463 | 1471 | |
| 1464 | 1472 | applyCharacterTagsToMessageDivs({ mesIds: lodash.range(startIndex, targetChat.length, 1) }); |
| 1465 | 1473 | } |
| 1466 | 1474 | |
| 1467 | 1475 | refreshSwipeButtons(false, fade); |
| @@ -1682,7 +1690,7 @@ export async function sendTextareaMessage() { | ||
| 1682 | 1690 | * @param {boolean} isSystem If the message was sent by the system |
| 1683 | 1691 | * @param {boolean} isUser If the message was sent by the user |
| 1684 | 1692 | * @param {number} messageId Message index in chat array |
| 1685 | 1693 | * @param {objectPartial<DOMPurify.Config>} [sanitizerOverrides] DOMPurify sanitizer option overrides |
| 1686 | 1694 | * @param {boolean} [isReasoning] If the message is reasoning output |
| 1687 | 1695 | * @returns {string} HTML string |
| 1688 | 1696 | */ |
| @@ -1831,7 +1839,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | ||
| 1831 | 1839 | mes = mes.replace(new RegExp(`(^|\n)${escapeRegex(ch_name)}:`, 'g'), '$1'); |
| 1832 | 1840 | } |
| 1833 | 1841 | |
| 1834 | 1842 | /** @type {import('dompurify')DOMPurify.Config & { RETURN_DOM_FRAGMENT: false; RETURN_DOM: false }} */ |
| 1835 | 1843 | const config = { |
| 1836 | 1844 | RETURN_DOM: false, |
| 1837 | 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 | 2428 | * Adds a single message to the chat. |
| @@ -2391,18 +2439,27 @@ export function addCopyToCodeBlocks(messageElement) { | ||
| 2391 | 2439 | */ |
| 2392 | 2440 | export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true, insert = true } = {}) { |
| 2393 | 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 | 2459 | const momentDate = timestampToMoment(mes.send_date); |
| 2398 | 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 | 2462 | let avatarImg = getThumbnailUrl('persona', user_avatar); |
| 2405 | - const isSystem = mes.is_system; | |
| 2406 | 2463 | |
| 2407 | 2464 | //for non-user messages |
| 2408 | 2465 | if (!mes.is_user) { |
| @@ -2426,18 +2483,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2426 | 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 | 2487 | let newMessage; |
| 2442 | 2488 | |
| 2443 | 2489 | if (type === 'swipe') { |
| @@ -2488,7 +2534,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2488 | 2534 | insertSVGIcon(newMessage, mes.extra); |
| 2489 | 2535 | } |
| 2490 | 2536 | |
| 2491 | - | |
| 2492 | 2537 | if (type !== 'swipe' && insert) { |
| 2493 | 2538 | if (!insertAfter && !insertBefore) { |
| 2494 | 2539 | chatElement.append(newMessage); |
| @@ -2512,13 +2557,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2512 | 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 | 2562 | newMessage.find('.avatar img').on('error', function () { |
| 2524 | 2563 | $(this).hide(); |
| @@ -2526,8 +2565,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2526 | 2565 | }); |
| 2527 | 2566 | |
| 2528 | 2567 | appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE); |
| 2529 | 2568 | newMessage.find('.mes_text').html(messageTextmessageHTML); |
| 2530 | - | |
| 2531 | 2569 | addCopyToCodeBlocks(newMessage); |
| 2532 | 2570 | |
| 2533 | 2571 | // Set the swipes counter for all non-user messages. |
| @@ -11651,14 +11689,16 @@ jQuery(async function () { | ||
| 11651 | 11689 | const oldScroll = chatElement[0].scrollTop; |
| 11652 | 11690 | const clone = structuredClone(chat[this_edit_mes_id]); |
| 11653 | 11691 | clone.send_date = Date.now(); |
| 11654 | 11692 | clone.mesconst this_edit_mes_element = $(this).closest('.mes').find('.edit_textarea').val().toString(); |
| 11693 | + clone.mes = this_edit_mes_element.find('.edit_textarea').val().toString(); | |
| 11655 | 11694 | |
| 11656 | 11695 | if (power_user.trim_spaces) { |
| 11657 | 11696 | clone.mes = clone.mes.trim(); |
| 11658 | 11697 | } |
| 11659 | 11698 | |
| 11660 | 11699 | chat.splice(Number(this_edit_mes_id) + 1, 0, clone); |
| 11661 | 11700 | const newMessageElement = addOneMessage(clone, { insertAfterinsert: this_edit_mes_idfalse }); |
| 11701 | + this_edit_mes_element.after(newMessageElement); | |
| 11662 | 11702 | |
| 11663 | 11703 | updateViewMessageIds(); |
| 11664 | 11704 | await saveChatConditional(); |
| @@ -685,7 +685,7 @@ export function formatCreatorNotes(text, avatarId) { | ||
| 685 | 685 | const preference = new StylesPreference(avatarId); |
| 686 | 686 | const sanitizeStyles = !preference.get(); |
| 687 | 687 | const decodeStyleParam = { prefix: sanitizeStyles ? '#creator_notes_spoiler ' : '' }; |
| 688 | 688 | /** @type {import('dompurify')DOMPurify.Config} */ |
| 689 | 689 | const config = { |
| 690 | 690 | RETURN_DOM: false, |
| 691 | 691 | RETURN_DOM_FRAGMENT: false, |
| @@ -4642,7 +4642,8 @@ async function messageRoleCallback(args, role) { | ||
| 4642 | 4642 | await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt); |
| 4643 | 4643 | const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`); |
| 4644 | 4644 | if (existingMessage.length) { |
| 4645 | 4645 | const newMessageElement = addOneMessage(message, { forceId: modifyAt, insertAfterinsert: modifyAtfalse, scroll: false }); |
| 4646 | + existingMessage.after(newMessageElement); | |
| 4646 | 4647 | existingMessage.remove(); |
| 4647 | 4648 | } |
| 4648 | 4649 | await eventSource.emit(event_types.MESSAGE_UPDATED, modifyAt); |
| @@ -4707,7 +4708,8 @@ async function messageNameCallback(args, name) { | ||
| 4707 | 4708 | await eventSource.emit(event_types.MESSAGE_EDITED, modifyAt); |
| 4708 | 4709 | const existingMessage = chatElement.find(`.mes[mesid="${modifyAt}"]`); |
| 4709 | 4710 | if (existingMessage.length) { |
| 4710 | 4711 | const newMessageElement = addOneMessage(message, { forceId: modifyAt, insertAfterinsert: modifyAtfalse, scroll: false }); |
| 4712 | + existingMessage.after(newMessageElement); | |
| 4711 | 4713 | existingMessage.remove(); |
| 4712 | 4714 | } |
| 4713 | 4715 | await eventSource.emit(event_types.MESSAGE_UPDATED, modifyAt); |