🧵 fix: Prevent Unnecessary Re-renders when Loading Chats (#5189)

* chore: typing

* chore: typing

* fix: enhance message scrolling logic to handle empty messages tree and ref checks

* fix: optimize message selection logic with useCallback for better performance

* chore: typing

* refactor: optimize icon rendering

* refactor: further optimize chat props

* fix: remove unnecessary console log in useQueryParams cleanup

* refactor: add queryClient to reset message data on new conversation initiation

* refactor: update data-testid attributes for consistency and improve code readability

* refactor: integrate queryClient to reset message data on new conversation initiation
This commit is contained in:
Danny Avila 2025-01-06 10:32:44 -05:00 committed by GitHub
parent 7987e04a2c
commit b01c744eb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 184 additions and 88 deletions

View file

@ -1,8 +1,9 @@
import { memo } from 'react'; import { memo, useCallback } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query'; import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
import type { TMessage } from 'librechat-data-provider';
import type { ChatFormValues } from '~/common'; import type { ChatFormValues } from '~/common';
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers'; import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks'; import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
@ -24,10 +25,13 @@ function ChatView({ index = 0 }: { index?: number }) {
const fileMap = useFileMapContext(); const fileMap = useFileMapContext();
const { data: messagesTree = null, isLoading } = useGetMessagesByConvoId(conversationId ?? '', { const { data: messagesTree = null, isLoading } = useGetMessagesByConvoId(conversationId ?? '', {
select: (data) => { select: useCallback(
(data: TMessage[]) => {
const dataTree = buildTree({ messages: data, fileMap }); const dataTree = buildTree({ messages: data, fileMap });
return dataTree?.length === 0 ? null : dataTree ?? null; return dataTree?.length === 0 ? null : dataTree ?? null;
}, },
[fileMap],
),
enabled: !!fileMap, enabled: !!fileMap,
}); });

View file

@ -1,9 +1,13 @@
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import { useMediaQuery, useLocalize } from '~/hooks';
import { NewChatIcon } from '~/components/svg'; import { NewChatIcon } from '~/components/svg';
import { useChatContext } from '~/Providers'; import { useChatContext } from '~/Providers';
import { useMediaQuery, useLocalize } from '~/hooks';
export default function HeaderNewChat() { export default function HeaderNewChat() {
const { newConversation } = useChatContext(); const queryClient = useQueryClient();
const { conversation, newConversation } = useChatContext();
const isSmallScreen = useMediaQuery('(max-width: 768px)'); const isSmallScreen = useMediaQuery('(max-width: 768px)');
const localize = useLocalize(); const localize = useLocalize();
if (isSmallScreen) { if (isSmallScreen) {
@ -12,10 +16,16 @@ export default function HeaderNewChat() {
return ( return (
<button <button
data-testid="wide-header-new-chat-button" data-testid="wide-header-new-chat-button"
aria-label={localize("com_ui_new_chat")} aria-label={localize('com_ui_new_chat')}
type="button" type="button"
className="btn btn-neutral btn-small border-token-border-medium relative ml-2 flex hidden h-9 w-9 items-center justify-center whitespace-nowrap rounded-lg rounded-lg border focus:border-black-500 dark:focus:border-white-500 md:flex" className="btn btn-neutral btn-small border-token-border-medium focus:border-black-500 dark:focus:border-white-500 relative ml-2 flex h-9 w-9 items-center justify-center whitespace-nowrap rounded-lg border md:flex"
onClick={() => newConversation()} onClick={() => {
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConversation();
}}
> >
<div className="flex w-full items-center justify-center gap-2"> <div className="flex w-full items-center justify-center gap-2">
<NewChatIcon /> <NewChatIcon />

View file

@ -1,26 +1,23 @@
import React, { useMemo, memo } from 'react'; import React, { useMemo, memo } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TMessage, TPreset, Assistant, Agent } from 'librechat-data-provider'; import type { Assistant, Agent, TMessage } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL'; import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { getEndpointField, getIconEndpoint } from '~/utils'; import { getEndpointField, getIconEndpoint } from '~/utils';
import Icon from '~/components/Endpoints/Icon'; import Icon from '~/components/Endpoints/Icon';
const MessageIcon = memo( const MessageIcon = memo(
( (props: {
props: Pick<TMessageProps, 'message' | 'conversation'> & { iconData?: TMessage & { modelLabel?: string };
assistant?: Assistant; assistant?: Assistant;
agent?: Agent; agent?: Agent;
}, }) => {
) => {
const { data: endpointsConfig } = useGetEndpointsQuery(); const { data: endpointsConfig } = useGetEndpointsQuery();
const { message, conversation, assistant, agent } = props; const { iconData, assistant, agent } = props;
const assistantName = useMemo(() => assistant?.name ?? '', [assistant]); const assistantName = useMemo(() => assistant?.name ?? '', [assistant]);
const assistantAvatar = useMemo(() => assistant?.metadata?.avatar ?? '', [assistant]); const assistantAvatar = useMemo(() => assistant?.metadata?.avatar ?? '', [assistant]);
const agentName = useMemo(() => props.agent?.name ?? '', [props.agent]); const agentName = useMemo(() => props.agent?.name ?? '', [props.agent]);
const agentAvatar = useMemo(() => props.agent?.avatar?.filepath ?? '', [props.agent]); const agentAvatar = useMemo(() => props.agent?.avatar?.filepath ?? '', [props.agent]);
const isCreatedByUser = useMemo(() => message?.isCreatedByUser ?? false, [message]);
let avatarURL = ''; let avatarURL = '';
@ -30,21 +27,10 @@ const MessageIcon = memo(
avatarURL = agentAvatar; avatarURL = agentAvatar;
} }
const messageSettings = useMemo( const iconURL = iconData?.iconURL;
() => ({
...(conversation ?? {}),
...({
...(message ?? {}),
iconURL: message?.iconURL ?? '',
} as TMessage),
}),
[conversation, message],
);
const iconURL = messageSettings.iconURL;
const endpoint = useMemo( const endpoint = useMemo(
() => getIconEndpoint({ endpointsConfig, iconURL, endpoint: messageSettings.endpoint }), () => getIconEndpoint({ endpointsConfig, iconURL, endpoint: iconData?.endpoint }),
[endpointsConfig, iconURL, messageSettings.endpoint], [endpointsConfig, iconURL, iconData?.endpoint],
); );
const endpointIconURL = useMemo( const endpointIconURL = useMemo(
@ -52,10 +38,11 @@ const MessageIcon = memo(
[endpointsConfig, endpoint], [endpointsConfig, endpoint],
); );
if (isCreatedByUser !== true && iconURL != null && iconURL.includes('http')) { if (iconData?.isCreatedByUser !== true && iconURL != null && iconURL.includes('http')) {
return ( return (
<ConvoIconURL <ConvoIconURL
preset={messageSettings as typeof messageSettings & TPreset} iconURL={iconURL}
modelLabel={iconData?.modelLabel}
context="message" context="message"
assistantAvatar={assistantAvatar} assistantAvatar={assistantAvatar}
agentAvatar={agentAvatar} agentAvatar={agentAvatar}
@ -68,10 +55,10 @@ const MessageIcon = memo(
return ( return (
<Icon <Icon
isCreatedByUser={isCreatedByUser} isCreatedByUser={iconData?.isCreatedByUser ?? false}
endpoint={endpoint} endpoint={endpoint}
iconURL={avatarURL || endpointIconURL} iconURL={avatarURL || endpointIconURL}
model={message?.model ?? conversation?.model} model={iconData?.model}
assistantName={assistantName} assistantName={assistantName}
agentName={agentName} agentName={agentName}
size={28.8} size={28.8}

View file

@ -1,7 +1,8 @@
import React, { useMemo } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import type { TMessageContentParts } from 'librechat-data-provider'; import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps } from '~/common'; import type { TMessageProps } from '~/common';
import Icon from '~/components/Chat/Messages/MessageIcon'; import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import { useMessageHelpers, useLocalize } from '~/hooks'; import { useMessageHelpers, useLocalize } from '~/hooks';
import ContentParts from './Content/ContentParts'; import ContentParts from './Content/ContentParts';
import SiblingSwitch from './SiblingSwitch'; import SiblingSwitch from './SiblingSwitch';
@ -35,6 +36,26 @@ export default function Message(props: TMessageProps) {
const fontSize = useRecoilValue(store.fontSize); const fontSize = useRecoilValue(store.fontSize);
const { children, messageId = null, isCreatedByUser } = message ?? {}; const { children, messageId = null, isCreatedByUser } = message ?? {};
const iconData = useMemo(
() =>
({
endpoint: conversation?.endpoint,
model: conversation?.model ?? message?.model,
iconURL: conversation?.iconURL ?? message?.iconURL ?? '',
modelLabel: conversation?.chatGptLabel ?? conversation?.modelLabel,
isCreatedByUser: message?.isCreatedByUser,
} as TMessage & { modelLabel?: string }),
[
conversation?.chatGptLabel,
conversation?.modelLabel,
conversation?.endpoint,
conversation?.iconURL,
conversation?.model,
message?.model,
message?.iconURL,
message?.isCreatedByUser,
],
);
if (!message) { if (!message) {
return null; return null;
} }
@ -62,12 +83,7 @@ export default function Message(props: TMessageProps) {
<div> <div>
<div className="pt-0.5"> <div className="pt-0.5">
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full"> <div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon <MessageIcon iconData={iconData} assistant={assistant} agent={agent} />
message={message}
conversation={conversation}
assistant={assistant}
agent={agent}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -56,7 +56,7 @@ export default function MessagesView({
</div> </div>
) : ( ) : (
<> <>
{Header && Header} {Header != null && Header}
<div ref={screenshotTargetRef}> <div ref={screenshotTargetRef}>
<MultiMessage <MultiMessage
key={conversationId} // avoid internal state mixture key={conversationId} // avoid internal state mixture

View file

@ -1,5 +1,7 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useAuthContext, useLocalize } from '~/hooks'; import { useAuthContext, useLocalize } from '~/hooks';
import type { TMessage } from 'librechat-data-provider';
import type { TMessageProps } from '~/common'; import type { TMessageProps } from '~/common';
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons'; import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon'; import Icon from '~/components/Chat/Messages/MessageIcon';
@ -15,6 +17,17 @@ export default function Message({ message }: Pick<TMessageProps, 'message'>) {
const { user } = useAuthContext(); const { user } = useAuthContext();
const localize = useLocalize(); const localize = useLocalize();
const iconData = useMemo(
() =>
({
endpoint: message?.endpoint,
model: message?.model,
iconURL: message?.iconURL ?? '',
isCreatedByUser: message?.isCreatedByUser,
} as TMessage & { modelLabel?: string }),
[message?.model, message?.iconURL, message?.endpoint, message?.isCreatedByUser],
);
if (!message) { if (!message) {
return null; return null;
} }
@ -27,7 +40,7 @@ export default function Message({ message }: Pick<TMessageProps, 'message'>) {
? (user?.name ?? '') || (user?.username ?? '') ? (user?.name ?? '') || (user?.username ?? '')
: localize('com_user_message'); : localize('com_user_message');
} else { } else {
messageLabel = message.sender || ''; messageLabel = message.sender ?? '';
} }
return ( return (
@ -39,7 +52,7 @@ export default function Message({ message }: Pick<TMessageProps, 'message'>) {
<div> <div>
<div className="pt-0.5"> <div className="pt-0.5">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full"> <div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon message={message} /> <Icon iconData={iconData} />
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,7 +6,7 @@ import MessageContent from '~/components/Chat/Messages/Content/MessageContent';
import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow'; import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch'; import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import HoverButtons from '~/components/Chat/Messages/HoverButtons'; import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon'; import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import { Plugin } from '~/components/Messages/Content'; import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow'; import SubRow from '~/components/Chat/Messages/SubRow';
import { MessageContext } from '~/Providers'; import { MessageContext } from '~/Providers';
@ -66,6 +66,27 @@ const MessageRender = memo(
[hasNoChildren, msg?.depth, latestMessage?.depth], [hasNoChildren, msg?.depth, latestMessage?.depth],
); );
const iconData = useMemo(
() =>
({
endpoint: conversation?.endpoint,
model: conversation?.model ?? msg?.model,
iconURL: conversation?.iconURL ?? msg?.iconURL ?? '',
modelLabel: conversation?.chatGptLabel ?? conversation?.modelLabel,
isCreatedByUser: msg?.isCreatedByUser,
} as TMessage & { modelLabel?: string }),
[
conversation?.chatGptLabel,
conversation?.modelLabel,
conversation?.endpoint,
conversation?.iconURL,
conversation?.model,
msg?.model,
msg?.iconURL,
msg?.isCreatedByUser,
],
);
if (!msg) { if (!msg) {
return null; return null;
} }
@ -125,7 +146,7 @@ const MessageRender = memo(
<div> <div>
<div className="pt-0.5"> <div className="pt-0.5">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full"> <div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon message={msg} conversation={conversation} assistant={assistant} /> <MessageIcon iconData={iconData} assistant={assistant} />
</div> </div>
</div> </div>
</div> </div>

View file

@ -52,7 +52,8 @@ export default function ConvoIcon({
<> <>
{iconURL && iconURL.includes('http') ? ( {iconURL && iconURL.includes('http') ? (
<ConvoIconURL <ConvoIconURL
preset={conversation} iconURL={iconURL}
modelLabel={conversation?.chatGptLabel ?? conversation?.modelLabel ?? ''}
endpointIconURL={endpointIconURL} endpointIconURL={endpointIconURL}
assistantAvatar={avatar} assistantAvatar={avatar}
assistantName={name} assistantName={name}

View file

@ -1,10 +1,10 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import type { TPreset } from 'librechat-data-provider';
import type { IconMapProps } from '~/common'; import type { IconMapProps } from '~/common';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons'; import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
interface ConvoIconURLProps { interface ConvoIconURLProps {
preset: TPreset | null; iconURL?: string;
modelLabel?: string;
endpointIconURL?: string; endpointIconURL?: string;
assistantName?: string; assistantName?: string;
agentName?: string; agentName?: string;
@ -29,7 +29,8 @@ const styleImageMap = {
}; };
const ConvoIconURL: React.FC<ConvoIconURLProps> = ({ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
preset, iconURL = '',
modelLabel = '',
endpointIconURL, endpointIconURL,
assistantAvatar, assistantAvatar,
assistantName, assistantName,
@ -37,7 +38,6 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
agentName, agentName,
context, context,
}) => { }) => {
const { iconURL = '' } = preset ?? {};
let Icon: ( let Icon: (
props: IconMapProps & { props: IconMapProps & {
context?: string; context?: string;
@ -57,7 +57,7 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
> >
<img <img
src={iconURL} src={iconURL}
alt={preset?.chatGptLabel ?? preset?.modelLabel ?? ''} alt={modelLabel}
style={styleImageMap[context ?? 'default'] ?? styleImageMap.default} style={styleImageMap[context ?? 'default'] ?? styleImageMap.default}
className="object-cover" className="object-cover"
/> />

View file

@ -42,10 +42,8 @@ export default function EndpointIcon({
if (iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))) { if (iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))) {
return ( return (
<ConvoIconURL <ConvoIconURL
preset={{ iconURL={iconURL}
...(conversation as TPreset), modelLabel={conversation?.chatGptLabel ?? conversation?.modelLabel ?? ''}
iconURL,
}}
context={context} context={context}
endpointIconURL={endpointIconURL} endpointIconURL={endpointIconURL}
assistantAvatar={assistantAvatar} assistantAvatar={assistantAvatar}

View file

@ -6,7 +6,7 @@ import ContentParts from '~/components/Chat/Messages/Content/ContentParts';
import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow'; import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch'; import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import HoverButtons from '~/components/Chat/Messages/HoverButtons'; import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon'; import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import SubRow from '~/components/Chat/Messages/SubRow'; import SubRow from '~/components/Chat/Messages/SubRow';
import { useMessageActions } from '~/hooks'; import { useMessageActions } from '~/hooks';
import { cn, logger } from '~/utils'; import { cn, logger } from '~/utils';
@ -65,6 +65,27 @@ const ContentRender = memo(
[msg?.children, msg?.depth, latestMessage?.depth], [msg?.children, msg?.depth, latestMessage?.depth],
); );
const iconData = useMemo(
() =>
({
endpoint: conversation?.endpoint,
model: conversation?.model ?? msg?.model,
iconURL: conversation?.iconURL ?? msg?.iconURL ?? '',
modelLabel: conversation?.chatGptLabel ?? conversation?.modelLabel,
isCreatedByUser: msg?.isCreatedByUser,
} as TMessage & { modelLabel?: string }),
[
conversation?.chatGptLabel,
conversation?.modelLabel,
conversation?.endpoint,
conversation?.iconURL,
conversation?.model,
msg?.model,
msg?.iconURL,
msg?.isCreatedByUser,
],
);
if (!msg) { if (!msg) {
return null; return null;
} }
@ -109,12 +130,7 @@ const ContentRender = memo(
<div> <div>
<div className="pt-0.5"> <div className="pt-0.5">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full"> <div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon <MessageIcon iconData={iconData} assistant={assistant} agent={agent} />
message={msg}
conversation={conversation}
assistant={assistant}
agent={agent}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,5 +1,8 @@
import React from 'react'; import React from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import { useLocalize, useNewConvo } from '~/hooks'; import { useLocalize, useNewConvo } from '~/hooks';
import store from '~/store'; import store from '~/store';
@ -10,6 +13,7 @@ export default function MobileNav({
setNavVisible: Dispatch<SetStateAction<boolean>>; setNavVisible: Dispatch<SetStateAction<boolean>>;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const queryClient = useQueryClient();
const { newConversation } = useNewConvo(0); const { newConversation } = useNewConvo(0);
const conversation = useRecoilValue(store.conversationByIndex(0)); const conversation = useRecoilValue(store.conversationByIndex(0));
const { title = 'New Chat' } = conversation || {}; const { title = 'New Chat' } = conversation || {};
@ -46,13 +50,19 @@ export default function MobileNav({
</svg> </svg>
</button> </button>
<h1 className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-center text-sm font-normal"> <h1 className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-center text-sm font-normal">
{title || localize('com_ui_new_chat')} {title ?? localize('com_ui_new_chat')}
</h1> </h1>
<button <button
type="button" type="button"
aria-label={localize('com_ui_new_chat')} aria-label={localize('com_ui_new_chat')}
className="m-1 inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover" className="m-1 inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover"
onClick={() => newConversation()} onClick={() => {
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConversation();
}}
> >
<svg <svg
width="24" width="24"

View file

@ -1,8 +1,10 @@
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TConversation } from 'librechat-data-provider'; import type { TConversation, TMessage } from 'librechat-data-provider';
import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils'; import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons'; import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL'; import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
@ -35,7 +37,12 @@ const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | nul
return ( return (
<div className="h-7 w-7 flex-shrink-0"> <div className="h-7 w-7 flex-shrink-0">
{iconURL && iconURL.includes('http') ? ( {iconURL && iconURL.includes('http') ? (
<ConvoIconURL preset={conversation} endpointIconURL={iconURL} context="nav" /> <ConvoIconURL
iconURL={iconURL}
modelLabel={conversation?.chatGptLabel ?? conversation?.modelLabel ?? ''}
endpointIconURL={iconURL}
context="nav"
/>
) : ( ) : (
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black"> <div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black">
{endpoint && Icon != null && ( {endpoint && Icon != null && (
@ -65,6 +72,7 @@ export default function NewChat({
subHeaders?: React.ReactNode; subHeaders?: React.ReactNode;
isSmallScreen: boolean; isSmallScreen: boolean;
}) { }) {
const queryClient = useQueryClient();
/** Note: this component needs an explicit index passed if using more than one */ /** Note: this component needs an explicit index passed if using more than one */
const { newConversation: newConvo } = useNewConvo(index); const { newConversation: newConvo } = useNewConvo(index);
const navigate = useNavigate(); const navigate = useNavigate();
@ -75,6 +83,10 @@ export default function NewChat({
const clickHandler = (event: React.MouseEvent<HTMLAnchorElement>) => { const clickHandler = (event: React.MouseEvent<HTMLAnchorElement>) => {
if (event.button === 0 && !(event.ctrlKey || event.metaKey)) { if (event.button === 0 && !(event.ctrlKey || event.metaKey)) {
event.preventDefault(); event.preventDefault();
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConvo(); newConvo();
navigate('/c/new'); navigate('/c/new');
toggleNav(); toggleNav();
@ -87,7 +99,7 @@ export default function NewChat({
<a <a
href="/" href="/"
tabIndex={0} tabIndex={0}
data-testid="nav-new-chat" data-testid="nav-new-chat-button"
onClick={clickHandler} onClick={clickHandler}
className={cn( className={cn(
'group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover', 'group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover',

View file

@ -42,7 +42,7 @@ export default function Message(props: TMessageProps) {
if (isCreatedByUser) { if (isCreatedByUser) {
messageLabel = 'anonymous'; messageLabel = 'anonymous';
} else { } else {
messageLabel = message.sender || ''; messageLabel = message.sender ?? '';
} }
return ( return (

View file

@ -30,14 +30,15 @@ export default function MessageIcon(
[conversation, message], [conversation, message],
); );
const iconURL = messageSettings?.iconURL; const iconURL = messageSettings.iconURL ?? '';
let endpoint = messageSettings?.endpoint; let endpoint = messageSettings.endpoint;
endpoint = getIconEndpoint({ endpointsConfig: undefined, iconURL, endpoint }); endpoint = getIconEndpoint({ endpointsConfig: undefined, iconURL, endpoint });
if (!message?.isCreatedByUser && iconURL && iconURL.includes('http')) { if (message?.isCreatedByUser !== true && iconURL && iconURL.includes('http')) {
return ( return (
<ConvoIconURL <ConvoIconURL
preset={messageSettings as typeof messageSettings & TPreset} iconURL={iconURL}
modelLabel={messageSettings.chatGptLabel ?? messageSettings.modelLabel ?? ''}
context="message" context="message"
assistantAvatar={assistantAvatar} assistantAvatar={assistantAvatar}
assistantName={assistantName} assistantName={assistantName}
@ -47,7 +48,7 @@ export default function MessageIcon(
); );
} }
if (message?.isCreatedByUser) { if (message?.isCreatedByUser === true) {
return ( return (
<div <div
style={{ style={{
@ -67,7 +68,7 @@ export default function MessageIcon(
<MessageEndpointIcon <MessageEndpointIcon
{...messageSettings} {...messageSettings}
endpoint={endpoint} endpoint={endpoint}
iconURL={!assistant ? undefined : assistantAvatar} iconURL={assistant == null ? undefined : assistantAvatar}
model={message?.model ?? conversation?.model} model={message?.model ?? conversation?.model}
assistantName={assistantName} assistantName={assistantName}
agentName={agentName} agentName={agentName}

View file

@ -116,7 +116,7 @@ export default function useChatHelpers(index = 0, paramId?: string) {
const handleRegenerate = (e: React.MouseEvent<HTMLButtonElement>) => { const handleRegenerate = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
const parentMessageId = latestMessage?.parentMessageId; const parentMessageId = latestMessage?.parentMessageId ?? '';
if (!parentMessageId) { if (!parentMessageId) {
console.error('Failed to regenerate the message: parentMessageId not found.'); console.error('Failed to regenerate the message: parentMessageId not found.');
return; return;

View file

@ -207,7 +207,6 @@ export default function useQueryParams({
return () => { return () => {
clearInterval(intervalId); clearInterval(intervalId);
console.log('Cleanup: `useQueryParams` interval cleared');
}; };
}, [searchParams, methods, textAreaRef, newQueryConvo, newConversation]); }, [searchParams, methods, textAreaRef, newQueryConvo, newConversation]);
} }

View file

@ -72,22 +72,30 @@ export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
}); });
useEffect(() => { useEffect(() => {
if (!messagesTree) { if (!messagesTree || messagesTree.length === 0) {
return; return;
} }
if (isSubmitting && scrollToBottom && !abortScroll) { if (!messagesEndRef.current || !scrollableRef.current) {
return;
}
if (isSubmitting && scrollToBottom && abortScroll !== true) {
scrollToBottom(); scrollToBottom();
} }
return () => { return () => {
if (abortScroll) { if (abortScroll === true) {
scrollToBottom && scrollToBottom.cancel(); scrollToBottom && scrollToBottom.cancel();
} }
}; };
}, [isSubmitting, messagesTree, scrollToBottom, abortScroll]); }, [isSubmitting, messagesTree, scrollToBottom, abortScroll]);
useEffect(() => { useEffect(() => {
if (!messagesEndRef.current || !scrollableRef.current) {
return;
}
if (scrollToBottom && autoScroll && conversationId !== Constants.NEW_CONVO) { if (scrollToBottom && autoScroll && conversationId !== Constants.NEW_CONVO) {
scrollToBottom(); scrollToBottom();
} }

View file

@ -141,7 +141,7 @@ const showStopButtonByIndex = atomFamily<boolean, string | number>({
default: false, default: false,
}); });
const abortScrollFamily = atomFamily({ const abortScrollFamily = atomFamily<boolean, string | number>({
key: 'abortScrollByIndex', key: 'abortScrollByIndex',
default: false, default: false,
effects: [ effects: [

View file

@ -78,7 +78,7 @@ test.describe('Messaging suite', () => {
expect(currentUrl).toBe(initialUrl); expect(currentUrl).toBe(initialUrl);
//cleanup the conversation //cleanup the conversation
await page.getByTestId('new-chat-button').click(); await page.getByTestId('nav-new-chat-button').click();
expect(page.url()).toBe(initialUrl); expect(page.url()).toBe(initialUrl);
// Click on the first conversation // Click on the first conversation
@ -158,7 +158,7 @@ test.describe('Messaging suite', () => {
const currentUrl = page.url(); const currentUrl = page.url();
const conversationId = currentUrl.split(basePath).pop() ?? ''; const conversationId = currentUrl.split(basePath).pop() ?? '';
expect(isUUID(conversationId)).toBeTruthy(); expect(isUUID(conversationId)).toBeTruthy();
await page.getByTestId('new-chat-button').click(); await page.getByTestId('nav-new-chat-button').click();
expect(page.url()).toBe(initialUrl); expect(page.url()).toBe(initialUrl);
}); });
}); });