mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🧵 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:
parent
7987e04a2c
commit
b01c744eb8
20 changed files with 184 additions and 88 deletions
|
@ -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(
|
||||||
const dataTree = buildTree({ messages: data, fileMap });
|
(data: TMessage[]) => {
|
||||||
return dataTree?.length === 0 ? null : dataTree ?? null;
|
const dataTree = buildTree({ messages: data, fileMap });
|
||||||
},
|
return dataTree?.length === 0 ? null : dataTree ?? null;
|
||||||
|
},
|
||||||
|
[fileMap],
|
||||||
|
),
|
||||||
enabled: !!fileMap,
|
enabled: !!fileMap,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue