From fbe341a171c1550aca58cf266eb127573dacfe8e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 10 Oct 2025 11:22:16 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20refactor:=20Latest=20Message=20Trac?= =?UTF-8?q?king=20with=20Robust=20Text=20Key=20Generation=20(#10059)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: enhance logging for latest message actions in message components * fix: Extract previous convoId from latest text in message helpers and process hooks - Updated `useMessageHelpers` and `useMessageProcess` to extract `convoId` from the previous text key for improved message handling. - Refactored `getLengthAndLastTenChars` to `getLengthAndLastNChars` for better flexibility in character length retrieval. - Introduced `getLatestContentForKey` function to streamline content extraction from messages. * chore: Enhance logging for clearing latest messages in conversation hooks * refactor: Update message key formatting for improved URL parameter handling - Modified `getLatestContentForKey` to change the format from `${text}-${i}` to `${text}&i=${i}` for better URL parameter structure. - Adjusted `getTextKey` to increase character length retrieval from 12 to 16 in `getLengthAndLastNChars` for enhanced text processing. * refactor: Simplify convoId extraction and enhance message formatting - Updated `useMessageHelpers` and `useMessageProcess` to extract `convoId` using a new format for improved clarity. - Refactored `getLatestContentForKey` to streamline content formatting and ensure consistent use of `Constants.COMMON_DIVIDER` for better message structure. - Removed redundant length and last character extraction logic from `getLengthAndLastNChars` for cleaner code. * chore: linting * chore: Simplify pre-commit hook by removing unnecessary lines --- .husky/pre-commit | 3 - .../Chat/Messages/ui/MessageRender.tsx | 5 +- .../src/components/Messages/ContentRender.tsx | 5 +- .../Conversations/useNavigateToConvo.tsx | 1 + .../src/hooks/Messages/useMessageHelpers.tsx | 20 +++- .../src/hooks/Messages/useMessageProcess.tsx | 18 +++- client/src/hooks/SSE/useEventHandlers.ts | 2 + client/src/hooks/useNewConvo.ts | 1 + client/src/utils/messages.ts | 92 +++++++++++++++---- 9 files changed, 118 insertions(+), 29 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 67f5b00272..23c736d1de 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,2 @@ -#!/usr/bin/env sh -set -e -. "$(dirname -- "$0")/_/husky.sh" [ -n "$CI" ] && exit 0 npx lint-staged --config ./.husky/lint-staged.config.js diff --git a/client/src/components/Chat/Messages/ui/MessageRender.tsx b/client/src/components/Chat/Messages/ui/MessageRender.tsx index ad38f1ee40..f056fccc98 100644 --- a/client/src/components/Chat/Messages/ui/MessageRender.tsx +++ b/client/src/components/Chat/Messages/ui/MessageRender.tsx @@ -97,7 +97,10 @@ const MessageRender = memo( () => showCardRender && !isLatestMessage ? () => { - logger.log(`Message Card click: Setting ${msg?.messageId} as latest message`); + logger.log( + 'latest_message', + `Message Card click: Setting ${msg?.messageId} as latest message`, + ); logger.dir(msg); setLatestMessage(msg!); } diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx index bdf051453e..ce88687d23 100644 --- a/client/src/components/Messages/ContentRender.tsx +++ b/client/src/components/Messages/ContentRender.tsx @@ -96,7 +96,10 @@ const ContentRender = memo( () => showCardRender && !isLatestMessage ? () => { - logger.log(`Message Card click: Setting ${msg?.messageId} as latest message`); + logger.log( + 'latest_message', + `Message Card click: Setting ${msg?.messageId} as latest message`, + ); logger.dir(msg); setLatestMessage(msg!); } diff --git a/client/src/hooks/Conversations/useNavigateToConvo.tsx b/client/src/hooks/Conversations/useNavigateToConvo.tsx index dd8ce11aa5..55f43fa820 100644 --- a/client/src/hooks/Conversations/useNavigateToConvo.tsx +++ b/client/src/hooks/Conversations/useNavigateToConvo.tsx @@ -51,6 +51,7 @@ const useNavigateToConvo = (index = 0) => { hasSetConversation.current = true; setSubmission(null); if (resetLatestMessage) { + logger.log('latest_message', 'Clearing all latest messages'); clearAllLatestMessages(); } diff --git a/client/src/hooks/Messages/useMessageHelpers.tsx b/client/src/hooks/Messages/useMessageHelpers.tsx index 264fe666d6..8343e97756 100644 --- a/client/src/hooks/Messages/useMessageHelpers.tsx +++ b/client/src/hooks/Messages/useMessageHelpers.tsx @@ -3,8 +3,8 @@ import { useEffect, useRef, useCallback, useMemo } from 'react'; import { Constants, isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider'; import type { TMessageProps } from '~/common'; import { useMessagesViewContext, useAssistantsMapContext, useAgentsMapContext } from '~/Providers'; +import { getTextKey, TEXT_KEY_DIVIDER, logger } from '~/utils'; import useCopyToClipboard from './useCopyToClipboard'; -import { getTextKey, logger } from '~/utils'; export default function useMessageHelpers(props: TMessageProps) { const latestText = useRef(''); @@ -49,15 +49,27 @@ export default function useMessageHelpers(props: TMessageProps) { messageId: message.messageId, convoId, }; + + /* Extracted convoId from previous textKey (format: messageId|||length|||lastChars|||convoId) */ + let previousConvoId: string | null = null; + if ( + latestText.current && + typeof latestText.current === 'string' && + latestText.current.length > 0 + ) { + const parts = latestText.current.split(TEXT_KEY_DIVIDER); + previousConvoId = parts[parts.length - 1] || null; + } + if ( textKey !== latestText.current || - (latestText.current && convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2]) + (convoId != null && previousConvoId != null && convoId !== previousConvoId) ) { - logger.log('[useMessageHelpers] Setting latest message: ', logInfo); + logger.log('latest_message', '[useMessageHelpers] Setting latest message: ', logInfo); latestText.current = textKey; setLatestMessage({ ...message }); } else { - logger.log('No change in latest message', logInfo); + logger.log('latest_message', 'No change in latest message', logInfo); } }, [isLast, message, setLatestMessage, conversation?.conversationId]); diff --git a/client/src/hooks/Messages/useMessageProcess.tsx b/client/src/hooks/Messages/useMessageProcess.tsx index ea5779a69f..30bec90d17 100644 --- a/client/src/hooks/Messages/useMessageProcess.tsx +++ b/client/src/hooks/Messages/useMessageProcess.tsx @@ -3,8 +3,8 @@ import { useRecoilValue } from 'recoil'; import { Constants } from 'librechat-data-provider'; import { useEffect, useRef, useCallback, useMemo, useState } from 'react'; import type { TMessage } from 'librechat-data-provider'; +import { getTextKey, TEXT_KEY_DIVIDER, logger } from '~/utils'; import { useMessagesViewContext } from '~/Providers'; -import { getTextKey, logger } from '~/utils'; import store from '~/store'; export default function useMessageProcess({ message }: { message?: TMessage | null }) { @@ -43,11 +43,21 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu messageId: message.messageId, convoId, }; + + /* Extracted convoId from previous textKey (format: messageId|||length|||lastChars|||convoId) */ + let previousConvoId: string | null = null; + if ( + latestText.current && + typeof latestText.current === 'string' && + latestText.current.length > 0 + ) { + const parts = latestText.current.split(TEXT_KEY_DIVIDER); + previousConvoId = parts[parts.length - 1] || null; + } + if ( textKey !== latestText.current || - (convoId != null && - latestText.current && - convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2]) + (convoId != null && previousConvoId != null && convoId !== previousConvoId) ) { logger.log('latest_message', '[useMessageProcess] Setting latest message; logInfo:', logInfo); latestText.current = textKey; diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index 1d860fbb7a..83c1ff1ad9 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -339,6 +339,7 @@ export default function useEventHandlers({ setShowStopButton(true); if (resetLatestMessage) { + logger.log('latest_message', 'syncHandler: resetting latest message'); resetLatestMessage(); } }, @@ -418,6 +419,7 @@ export default function useEventHandlers({ } if (resetLatestMessage) { + logger.log('latest_message', 'createdHandler: resetting latest message'); resetLatestMessage(); } scrollToEnd(() => setAbortScroll(false)); diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index ab2d177428..22ea5f327c 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -179,6 +179,7 @@ const useNewConvo = (index = 0) => { } setSubmission({} as TSubmission); if (!(keepLatestMessage ?? false)) { + logger.log('latest_message', 'Clearing all latest messages'); clearAllLatestMessages(); } if (isCancelled) { diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts index caae46d923..fe8ec36499 100644 --- a/client/src/utils/messages.ts +++ b/client/src/utils/messages.ts @@ -1,15 +1,7 @@ -import { ContentTypes, Constants } from 'librechat-data-provider'; +import { ContentTypes } from 'librechat-data-provider'; import type { TMessage, TMessageContentParts } from 'librechat-data-provider'; -export const getLengthAndLastTenChars = (str?: string): string => { - if (typeof str !== 'string' || str.length === 0) { - return '0'; - } - - const length = str.length; - const lastTenChars = str.slice(-10); - return `${length}${lastTenChars}`; -}; +export const TEXT_KEY_DIVIDER = '|||'; export const getLatestText = (message?: TMessage | null, includeIndex?: boolean): string => { if (!message) { @@ -65,16 +57,84 @@ export const getAllContentText = (message?: TMessage | null): string => { return ''; }; +const getLatestContentForKey = (message: TMessage): string => { + const formatText = (str: string, index: number): string => { + if (str.length === 0) { + return '0'; + } + const length = str.length; + const lastChars = str.slice(-16); + return `${length}${TEXT_KEY_DIVIDER}${lastChars}${TEXT_KEY_DIVIDER}${index}`; + }; + + if (message.text) { + return formatText(message.text, -1); + } + + if (!message.content || message.content.length === 0) { + return ''; + } + + for (let i = message.content.length - 1; i >= 0; i--) { + const part = message.content[i] as TMessageContentParts | undefined; + if (!part?.type) { + continue; + } + + const type = part.type; + let text = ''; + + // Handle THINK type - extract think content + if (type === ContentTypes.THINK && 'think' in part) { + text = typeof part.think === 'string' ? part.think : (part.think?.value ?? ''); + } + // Handle TEXT type + else if (type === ContentTypes.TEXT && 'text' in part) { + text = typeof part.text === 'string' ? part.text : (part.text?.value ?? ''); + } + // Handle ERROR type + else if (type === ContentTypes.ERROR && 'error' in part) { + text = String(part.error || 'err').slice(0, 30); + } + // Handle TOOL_CALL - use simple marker with type + else if (type === ContentTypes.TOOL_CALL && 'tool_call' in part) { + const tcType = part.tool_call?.type || 'x'; + const tcName = String(part.tool_call?.['name'] || 'unknown').slice(0, 20); + const tcArgs = String(part.tool_call?.['args'] || 'none').slice(0, 20); + const tcOutput = String(part.tool_call?.['output'] || 'none').slice(0, 20); + text = `tc_${tcType}_${tcName}_${tcArgs}_${tcOutput}`; + } + // Handle IMAGE_FILE - use simple marker with file_id suffix + else if (type === ContentTypes.IMAGE_FILE && 'image_file' in part) { + const fileId = part.image_file?.file_id || 'x'; + text = `if_${fileId.slice(-8)}`; + } + // Handle IMAGE_URL - use simple marker + else if (type === ContentTypes.IMAGE_URL) { + text = 'iu'; + } + // Handle AGENT_UPDATE - use simple marker with agentId suffix + else if (type === ContentTypes.AGENT_UPDATE && 'agent_update' in part) { + const agentId = String(part.agent_update?.agentId || 'x').slice(0, 30); + text = `au_${agentId}`; + } else { + text = type; + } + + if (text.length > 0) { + return formatText(text, i); + } + } + + return ''; +}; + export const getTextKey = (message?: TMessage | null, convoId?: string | null) => { if (!message) { return ''; } - const text = getLatestText(message, true); - return `${(message.messageId as string | null) ?? ''}${ - Constants.COMMON_DIVIDER - }${getLengthAndLastTenChars(text)}${Constants.COMMON_DIVIDER}${ - message.conversationId ?? convoId - }`; + const contentKey = getLatestContentForKey(message); + return `${(message.messageId as string | null) ?? ''}${TEXT_KEY_DIVIDER}${contentKey}${TEXT_KEY_DIVIDER}${message.conversationId ?? convoId}`; }; export const scrollToEnd = (callback?: () => void) => {