️ refactor: Optimize Rendering Performance for Icons, Conversations (#5234)

* refactor: HoverButtons and Fork components to use explicit props

* refactor: improve typing for Fork Component

* fix: memoize SpecIcon to avoid unnecessary re-renders

* feat: introduce URLIcon component and update SpecIcon for improved icon handling

* WIP: optimizing icons

* refactor: simplify modelLabel assignment in Message components

* refactor: memoize ConvoOptions component to optimize rendering performance
This commit is contained in:
Danny Avila 2025-01-09 15:40:10 -05:00 committed by GitHub
parent 687ab32bd3
commit 0f95604a67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 206 additions and 171 deletions

View file

@ -307,6 +307,12 @@ export type TMessageProps = {
setSiblingIdx?: ((value: number) => void | React.Dispatch<React.SetStateAction<number>>) | null; setSiblingIdx?: ((value: number) => void | React.Dispatch<React.SetStateAction<number>>) | null;
}; };
export type TMessageIcon = { endpoint?: string | null; isCreatedByUser?: boolean } & Pick<
t.TConversation,
'modelLabel'
> &
Pick<t.TMessage, 'model' | 'iconURL'>;
export type TInitialProps = { export type TInitialProps = {
text: string; text: string;
edit: boolean; edit: boolean;

View file

@ -1,3 +1,4 @@
import { memo } from 'react';
import { EModelEndpoint, KnownEndpoints } from 'librechat-data-provider'; import { EModelEndpoint, KnownEndpoints } from 'librechat-data-provider';
import { CustomMinimalIcon } from '~/components/svg'; import { CustomMinimalIcon } from '~/components/svg';
import { IconContext } from '~/common'; import { IconContext } from '~/common';
@ -53,7 +54,7 @@ const getKnownClass = ({
return cn(match, defaultClass); return cn(match, defaultClass);
}; };
export default function UnknownIcon({ function UnknownIcon({
className = '', className = '',
endpoint: _endpoint, endpoint: _endpoint,
iconURL = '', iconURL = '',
@ -93,3 +94,5 @@ export default function UnknownIcon({
/> />
); );
} }
export default memo(UnknownIcon);

View file

@ -1,8 +1,9 @@
import React from 'react'; import React, { memo } from 'react';
import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider'; import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider';
import type { IconMapProps } from '~/common'; import type { IconMapProps } from '~/common';
import { getModelSpecIconURL, getIconKey, getEndpointField } from '~/utils'; import { getModelSpecIconURL, getIconKey, getEndpointField } from '~/utils';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons'; import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import { URLIcon } from '~/components/Endpoints/URLIcon';
interface SpecIconProps { interface SpecIconProps {
currentSpec: TModelSpec; currentSpec: TModelSpec;
@ -16,24 +17,12 @@ const SpecIcon: React.FC<SpecIconProps> = ({ currentSpec, endpointsConfig }) =>
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL }); const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL });
let Icon: (props: IconMapProps) => React.JSX.Element; let Icon: (props: IconMapProps) => React.JSX.Element;
if (!iconURL?.includes('http')) { if (!iconURL.includes('http')) {
Icon = icons[iconKey] ?? icons.unknown; Icon = icons[iconKey] ?? icons.unknown;
} else if (iconURL) {
return <URLIcon iconURL={iconURL} altName={currentSpec.name} />;
} else { } else {
Icon = iconURL Icon = icons[endpoint ?? ''] ?? icons.unknown;
? () => (
<div
className="icon-xl mr-1 shrink-0 overflow-hidden rounded-full "
style={{ width: '20', height: '20' }}
>
<img
src={iconURL}
alt={currentSpec.name}
style={{ width: '100%', height: '100%' }}
className="object-cover"
/>
</div>
)
: icons[endpoint ?? ''] ?? icons.unknown;
} }
return ( return (
@ -42,9 +31,9 @@ const SpecIcon: React.FC<SpecIconProps> = ({ currentSpec, endpointsConfig }) =>
endpoint={endpoint} endpoint={endpoint}
context="menu-item" context="menu-item"
iconURL={endpointIconURL} iconURL={endpointIconURL}
className="icon-lg mr-1 shrink-0 dark:text-white" className="icon-lg mr-1 shrink-0 text-text-primary"
/> />
); );
}; };
export default SpecIcon; export default memo(SpecIcon);

View file

@ -50,9 +50,13 @@ export default function HoverButtons({
} = useGenerationsByLatest({ } = useGenerationsByLatest({
isEditing, isEditing,
isSubmitting, isSubmitting,
message, error: message.error,
endpoint: endpoint ?? '', endpoint: endpoint ?? '',
latestMessage, messageId: message.messageId,
searchResult: message.searchResult,
finish_reason: message.finish_reason,
isCreatedByUser: message.isCreatedByUser,
latestMessageId: latestMessage?.messageId,
}); });
if (!conversation) { if (!conversation) {
return null; return null;
@ -146,7 +150,7 @@ export default function HoverButtons({
messageId={message.messageId} messageId={message.messageId}
conversationId={conversation.conversationId} conversationId={conversation.conversationId}
forkingSupported={forkingSupported} forkingSupported={forkingSupported}
latestMessage={latestMessage} latestMessageId={latestMessage?.messageId}
/> />
{continueSupported === true ? ( {continueSupported === true ? (
<button <button

View file

@ -1,31 +1,38 @@
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 { Assistant, Agent, TMessage } from 'librechat-data-provider'; import type { Assistant, Agent } from 'librechat-data-provider';
import type { TMessageIcon } from '~/common';
import { getEndpointField, getIconEndpoint, logger } from '~/utils';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL'; import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { getEndpointField, getIconEndpoint } from '~/utils';
import Icon from '~/components/Endpoints/Icon'; import Icon from '~/components/Endpoints/Icon';
const MessageIcon = memo( const MessageIcon = memo(
(props: { ({
iconData?: TMessage & { modelLabel?: string }; iconData,
assistant,
agent,
}: {
iconData?: TMessageIcon;
assistant?: Assistant; assistant?: Assistant;
agent?: Agent; agent?: Agent;
}) => { }) => {
logger.log('icon_data', iconData, assistant, agent);
const { data: endpointsConfig } = useGetEndpointsQuery(); const { data: endpointsConfig } = useGetEndpointsQuery();
const { iconData, assistant, agent } = props;
const agentName = useMemo(() => agent?.name ?? '', [agent]);
const agentAvatar = useMemo(() => agent?.avatar?.filepath ?? '', [agent]);
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 agentAvatar = useMemo(() => props.agent?.avatar?.filepath ?? '', [props.agent]);
let avatarURL = ''; const avatarURL = useMemo(() => {
let result = '';
if (assistant) { if (assistant) {
avatarURL = assistantAvatar; result = assistantAvatar;
} else if (agent) { } else if (agent) {
avatarURL = agentAvatar; result = agentAvatar;
} }
return result;
}, [assistant, agent, assistantAvatar, agentAvatar]);
const iconURL = iconData?.iconURL; const iconURL = iconData?.iconURL;
const endpoint = useMemo( const endpoint = useMemo(

View file

@ -1,7 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import type { TMessage, TMessageContentParts } from 'librechat-data-provider'; import type { TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps } from '~/common'; import type { TMessageProps, TMessageIcon } from '~/common';
import MessageIcon 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';
@ -35,19 +35,29 @@ export default function Message(props: TMessageProps) {
} = useMessageHelpers(props); } = useMessageHelpers(props);
const fontSize = useRecoilValue(store.fontSize); const fontSize = useRecoilValue(store.fontSize);
const { children, messageId = null, isCreatedByUser } = message ?? {}; const { children, messageId = null, isCreatedByUser } = message ?? {};
const name = useMemo(() => {
let result = '';
if (isCreatedByUser === true) {
result = localize('com_user_message');
} else if (assistant) {
result = assistant.name ?? localize('com_ui_assistant');
} else if (agent) {
result = agent.name ?? localize('com_ui_agent');
}
const iconData = useMemo( return result;
() => }, [assistant, agent, isCreatedByUser, localize]);
({
endpoint: message?.endpoint ?? conversation?.endpoint, const iconData: TMessageIcon = useMemo(
model: message?.model ?? conversation?.model, () => ({
iconURL: message?.iconURL ?? conversation?.iconURL, endpoint: message?.endpoint ?? conversation?.endpoint,
modelLabel: conversation?.chatGptLabel ?? conversation?.modelLabel, model: message?.model ?? conversation?.model,
isCreatedByUser: message?.isCreatedByUser, iconURL: message?.iconURL ?? conversation?.iconURL,
} as TMessage & { modelLabel?: string }), modelLabel: name,
isCreatedByUser: message?.isCreatedByUser,
}),
[ [
conversation?.chatGptLabel, name,
conversation?.modelLabel,
conversation?.endpoint, conversation?.endpoint,
conversation?.iconURL, conversation?.iconURL,
conversation?.model, conversation?.model,
@ -61,16 +71,6 @@ export default function Message(props: TMessageProps) {
return null; return null;
} }
let name = '';
if (isCreatedByUser === true) {
name = localize('com_user_message');
} else if (assistant) {
name = assistant.name ?? localize('com_ui_assistant');
} else if (agent) {
name = agent.name ?? localize('com_ui_agent');
}
return ( return (
<> <>
<div <div

View file

@ -1,8 +1,7 @@
import { useMemo } from 'react'; 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, TMessageIcon } 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';
import SearchContent from './Content/SearchContent'; import SearchContent from './Content/SearchContent';
@ -17,14 +16,13 @@ export default function Message({ message }: Pick<TMessageProps, 'message'>) {
const { user } = useAuthContext(); const { user } = useAuthContext();
const localize = useLocalize(); const localize = useLocalize();
const iconData = useMemo( const iconData: TMessageIcon = useMemo(
() => () => ({
({ endpoint: message?.endpoint,
endpoint: message?.endpoint, model: message?.model,
model: message?.model, iconURL: message?.iconURL ?? '',
iconURL: message?.iconURL ?? '', isCreatedByUser: message?.isCreatedByUser,
isCreatedByUser: message?.isCreatedByUser, }),
} as TMessage & { modelLabel?: string }),
[message?.model, message?.iconURL, message?.endpoint, message?.isCreatedByUser], [message?.model, message?.iconURL, message?.endpoint, message?.isCreatedByUser],
); );

View file

@ -1,7 +1,7 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useCallback, useMemo, memo } from 'react'; import { useCallback, useMemo, memo } from 'react';
import type { TMessage } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider';
import type { TMessageProps } from '~/common'; import type { TMessageProps, TMessageIcon } from '~/common';
import MessageContent from '~/components/Chat/Messages/Content/MessageContent'; 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';
@ -66,18 +66,16 @@ const MessageRender = memo(
[hasNoChildren, msg?.depth, latestMessage?.depth], [hasNoChildren, msg?.depth, latestMessage?.depth],
); );
const iconData = useMemo( const iconData: TMessageIcon = useMemo(
() => () => ({
({ endpoint: msg?.endpoint ?? conversation?.endpoint,
endpoint: msg?.endpoint ?? conversation?.endpoint, model: msg?.model ?? conversation?.model,
model: msg?.model ?? conversation?.model, iconURL: msg?.iconURL ?? conversation?.iconURL,
iconURL: msg?.iconURL ?? conversation?.iconURL, modelLabel: messageLabel,
modelLabel: conversation?.chatGptLabel ?? conversation?.modelLabel, isCreatedByUser: msg?.isCreatedByUser,
isCreatedByUser: msg?.isCreatedByUser, }),
} as TMessage & { modelLabel?: string }),
[ [
conversation?.chatGptLabel, messageLabel,
conversation?.modelLabel,
conversation?.endpoint, conversation?.endpoint,
conversation?.iconURL, conversation?.iconURL,
conversation?.model, conversation?.model,

View file

@ -1,4 +1,4 @@
import { useState, useId, useRef } from 'react'; import { useState, useId, useRef, memo } from 'react';
import * as Menu from '@ariakit/react/menu'; import * as Menu from '@ariakit/react/menu';
import { Ellipsis, Share2, Copy, Archive, Pen, Trash } from 'lucide-react'; import { Ellipsis, Share2, Copy, Archive, Pen, Trash } from 'lucide-react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query'; import { useGetStartupConfig } from 'librechat-data-provider/react-query';
@ -12,7 +12,7 @@ import DeleteButton from './DeleteButton';
import ShareButton from './ShareButton'; import ShareButton from './ShareButton';
import { cn } from '~/utils'; import { cn } from '~/utils';
export default function ConvoOptions({ function ConvoOptions({
conversationId, conversationId,
title, title,
retainView, retainView,
@ -161,3 +161,5 @@ export default function ConvoOptions({
</> </>
); );
} }
export default memo(ConvoOptions);

View file

@ -2,7 +2,7 @@ import { useState, useRef } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { GitFork, InfoIcon } from 'lucide-react'; import { GitFork, InfoIcon } from 'lucide-react';
import * as Popover from '@radix-ui/react-popover'; import * as Popover from '@radix-ui/react-popover';
import { ForkOptions, TMessage } from 'librechat-data-provider'; import { ForkOptions } from 'librechat-data-provider';
import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react'; import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react';
import { import {
Checkbox, Checkbox,
@ -26,9 +26,9 @@ interface PopoverButtonProps {
setActiveSetting: React.Dispatch<React.SetStateAction<string>>; setActiveSetting: React.Dispatch<React.SetStateAction<string>>;
sideOffset?: number; sideOffset?: number;
timeoutRef: React.MutableRefObject<NodeJS.Timeout | null>; timeoutRef: React.MutableRefObject<NodeJS.Timeout | null>;
hoverInfo?: React.ReactNode; hoverInfo?: React.ReactNode | string;
hoverTitle?: React.ReactNode; hoverTitle?: React.ReactNode | string;
hoverDescription?: React.ReactNode; hoverDescription?: React.ReactNode | string;
} }
const optionLabels = { const optionLabels = {
@ -73,7 +73,9 @@ const PopoverButton: React.FC<PopoverButtonProps> = ({
> >
{children} {children}
</Popover.Close> </Popover.Close>
{(hoverInfo || hoverTitle || hoverDescription) && ( {((hoverInfo != null && hoverInfo !== '') ||
(hoverTitle != null && hoverTitle !== '') ||
(hoverDescription != null && hoverDescription !== '')) && (
<HoverCardPortal> <HoverCardPortal>
<HoverCardContent <HoverCardContent
side="right" side="right"
@ -82,9 +84,11 @@ const PopoverButton: React.FC<PopoverButtonProps> = ({
> >
<div className="space-y-2"> <div className="space-y-2">
<p className="flex flex-col gap-2 text-sm text-gray-600 dark:text-gray-300"> <p className="flex flex-col gap-2 text-sm text-gray-600 dark:text-gray-300">
{hoverInfo && hoverInfo} {hoverInfo != null && hoverInfo !== '' && hoverInfo}
{hoverTitle && <span className="flex flex-wrap gap-1 font-bold">{hoverTitle}</span>} {hoverTitle != null && hoverTitle !== '' && (
{hoverDescription && hoverDescription} <span className="flex flex-wrap gap-1 font-bold">{hoverTitle}</span>
)}
{hoverDescription != null && hoverDescription !== '' && hoverDescription}
</p> </p>
</div> </div>
</HoverCardContent> </HoverCardContent>
@ -95,17 +99,17 @@ const PopoverButton: React.FC<PopoverButtonProps> = ({
}; };
export default function Fork({ export default function Fork({
isLast, isLast = false,
messageId, messageId,
conversationId, conversationId: _convoId,
forkingSupported, forkingSupported = false,
latestMessage, latestMessageId,
}: { }: {
isLast?: boolean; isLast?: boolean;
messageId: string; messageId: string;
conversationId: string | null; conversationId: string | null;
forkingSupported?: boolean; forkingSupported?: boolean;
latestMessage: TMessage | null; latestMessageId?: string;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const { index } = useChatContext(); const { index } = useChatContext();
@ -119,13 +123,11 @@ export default function Fork({
const [rememberGlobal, setRememberGlobal] = useRecoilState(store.rememberDefaultFork); const [rememberGlobal, setRememberGlobal] = useRecoilState(store.rememberDefaultFork);
const forkConvo = useForkConvoMutation({ const forkConvo = useForkConvoMutation({
onSuccess: (data) => { onSuccess: (data) => {
if (data) { navigateToConvo(data.conversation);
navigateToConvo(data.conversation); showToast({
showToast({ message: localize('com_ui_fork_success'),
message: localize('com_ui_fork_success'), status: 'success',
status: 'success', });
});
}
}, },
onMutate: () => { onMutate: () => {
showToast({ showToast({
@ -141,6 +143,7 @@ export default function Fork({
}, },
}); });
const conversationId = _convoId ?? '';
if (!forkingSupported || !conversationId || !messageId) { if (!forkingSupported || !conversationId || !messageId) {
return null; return null;
} }
@ -156,7 +159,7 @@ export default function Fork({
conversationId, conversationId,
option, option,
splitAtTarget, splitAtTarget,
latestMessageId: latestMessage?.messageId, latestMessageId,
}); });
}; };
@ -177,7 +180,7 @@ export default function Fork({
splitAtTarget, splitAtTarget,
conversationId, conversationId,
option: forkSetting, option: forkSetting,
latestMessageId: latestMessage?.messageId, latestMessageId,
}); });
} }
}} }}

View file

@ -1,10 +1,11 @@
import React, { memo } from 'react'; import { memo, useMemo } from 'react';
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';
import { URLIcon } from '~/components/Endpoints/URLIcon';
interface ConvoIconURLProps { interface ConvoIconURLProps {
iconURL?: string; iconURL?: string;
modelLabel?: string; modelLabel?: string | null;
endpointIconURL?: string; endpointIconURL?: string;
assistantName?: string; assistantName?: string;
agentName?: string; agentName?: string;
@ -38,33 +39,26 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
agentName, agentName,
context, context,
}) => { }) => {
let Icon: ( const Icon: (
props: IconMapProps & { props: IconMapProps & {
context?: string; context?: string;
iconURL?: string; iconURL?: string;
}, },
) => React.JSX.Element; ) => React.JSX.Element = useMemo(() => icons[iconURL] ?? icons.unknown, [iconURL]);
const isURL = useMemo(
const isURL = !!(iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))); () => !!(iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))),
[iconURL],
if (!isURL) { );
Icon = icons[iconURL] ?? icons.unknown; if (isURL) {
} else { return (
Icon = () => ( <URLIcon
<div iconURL={iconURL}
altName={modelLabel}
className={classMap[context ?? 'default'] ?? classMap.default} className={classMap[context ?? 'default'] ?? classMap.default}
style={styleMap[context ?? 'default'] ?? styleMap.default} containerStyle={styleMap[context ?? 'default'] ?? styleMap.default}
> imageStyle={styleImageMap[context ?? 'default'] ?? styleImageMap.default}
<img />
src={iconURL}
alt={modelLabel}
style={styleImageMap[context ?? 'default'] ?? styleImageMap.default}
className="object-cover"
/>
</div>
); );
return <Icon context={context} />;
} }
return ( return (

View file

@ -1,6 +1,6 @@
import { EModelEndpoint, isAssistantsEndpoint, alternateName } from 'librechat-data-provider'; import { memo } from 'react';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import { Feather } from 'lucide-react'; import { Feather } from 'lucide-react';
import { EModelEndpoint, isAssistantsEndpoint, alternateName } from 'librechat-data-provider';
import { import {
Plugin, Plugin,
GPTIcon, GPTIcon,
@ -13,10 +13,16 @@ import {
AzureMinimalIcon, AzureMinimalIcon,
CustomMinimalIcon, CustomMinimalIcon,
} from '~/components/svg'; } from '~/components/svg';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import { IconProps } from '~/common'; import { IconProps } from '~/common';
import { cn } from '~/utils'; import { cn } from '~/utils';
type EndpointIcon = {
icon: React.ReactNode | React.JSX.Element;
bg?: string;
name?: string | null;
};
function getOpenAIColor(_model: string | null | undefined) { function getOpenAIColor(_model: string | null | undefined) {
const model = _model?.toLowerCase() ?? ''; const model = _model?.toLowerCase() ?? '';
if (model && /\bo1\b/i.test(model)) { if (model && /\bo1\b/i.test(model)) {
@ -116,7 +122,9 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
name: endpoint, name: endpoint,
}; };
const endpointIcons = { const endpointIcons: {
[key: string]: EndpointIcon | undefined;
} = {
[EModelEndpoint.assistants]: assistantsIcon, [EModelEndpoint.assistants]: assistantsIcon,
[EModelEndpoint.agents]: agentsIcon, [EModelEndpoint.agents]: agentsIcon,
[EModelEndpoint.azureAssistants]: assistantsIcon, [EModelEndpoint.azureAssistants]: assistantsIcon,
@ -189,7 +197,9 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
}; };
let { icon, bg, name } = let { icon, bg, name } =
endpoint && endpointIcons[endpoint] ? endpointIcons[endpoint] : endpointIcons.default; endpoint != null && endpoint && endpointIcons[endpoint]
? endpointIcons[endpoint] ?? {}
: (endpointIcons.default as EndpointIcon);
if (iconURL && endpointIcons[iconURL]) { if (iconURL && endpointIcons[iconURL]) {
({ icon, bg, name } = endpointIcons[iconURL]); ({ icon, bg, name } = endpointIcons[iconURL]);
@ -201,9 +211,9 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
return ( return (
<div <div
title={name} title={name ?? ''}
style={{ style={{
background: bg || 'transparent', background: bg != null ? bg || 'transparent' : 'transparent',
width: size, width: size,
height: size, height: size,
}} }}
@ -222,4 +232,4 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
); );
}; };
export default MessageEndpointIcon; export default memo(MessageEndpointIcon);

View file

@ -0,0 +1,21 @@
import React, { memo } from 'react';
export const URLIcon = memo(
({
iconURL,
altName,
containerStyle = { width: '20', height: '20' },
imageStyle = { width: '100%', height: '100%' },
className = 'icon-xl mr-1 shrink-0 overflow-hidden rounded-full',
}: {
iconURL: string;
altName?: string | null;
className?: string;
containerStyle?: React.CSSProperties;
imageStyle?: React.CSSProperties;
}) => (
<div className={className} style={containerStyle}>
<img src={iconURL} alt={altName ?? ''} style={imageStyle} className="object-cover" />
</div>
),
);

View file

@ -1,7 +1,7 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useCallback, useMemo, memo } from 'react'; import { useCallback, useMemo, memo } from 'react';
import type { TMessage, TMessageContentParts } from 'librechat-data-provider'; import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps } from '~/common'; import type { TMessageProps, TMessageIcon } from '~/common';
import ContentParts from '~/components/Chat/Messages/Content/ContentParts'; 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';
@ -65,18 +65,16 @@ const ContentRender = memo(
[msg?.children, msg?.depth, latestMessage?.depth], [msg?.children, msg?.depth, latestMessage?.depth],
); );
const iconData = useMemo( const iconData: TMessageIcon = useMemo(
() => () => ({
({ endpoint: msg?.endpoint ?? conversation?.endpoint,
endpoint: msg?.endpoint ?? conversation?.endpoint, model: msg?.model ?? conversation?.model,
model: msg?.model ?? conversation?.model, iconURL: msg?.iconURL ?? conversation?.iconURL,
iconURL: msg?.iconURL ?? conversation?.iconURL, modelLabel: messageLabel,
modelLabel: conversation?.chatGptLabel ?? conversation?.modelLabel, isCreatedByUser: msg?.isCreatedByUser,
isCreatedByUser: msg?.isCreatedByUser, }),
} as TMessage & { modelLabel?: string }),
[ [
conversation?.chatGptLabel, messageLabel,
conversation?.modelLabel,
conversation?.endpoint, conversation?.endpoint,
conversation?.iconURL, conversation?.iconURL,
conversation?.model, conversation?.model,

View file

@ -220,6 +220,7 @@ export default function useChatFunctions({
isCreatedByUser: false, isCreatedByUser: false,
isEdited: isEditOrContinue, isEdited: isEditOrContinue,
iconURL: convo.iconURL, iconURL: convo.iconURL,
model: convo.model,
error: false, error: false,
}; };
@ -254,6 +255,7 @@ export default function useChatFunctions({
currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId); currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId);
} }
logger.log('message_state', initialResponse);
const submission: TSubmission = { const submission: TSubmission = {
conversation: { conversation: {
...conversation, ...conversation,

View file

@ -54,11 +54,11 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu
latestText.current && latestText.current &&
convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2]) convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2])
) { ) {
logger.log('[useMessageProcess] Setting latest message: ', logInfo); logger.log('latest_message', '[useMessageProcess] Setting latest message; logInfo:', logInfo);
latestText.current = textKey; latestText.current = textKey;
setLatestMessage({ ...message }); setLatestMessage({ ...message });
} else { } else {
logger.log('No change in latest message', logInfo); logger.log('latest_message', 'No change in latest message; logInfo', logInfo);
} }
}, [hasNoChildren, message, setLatestMessage, conversation?.conversationId]); }, [hasNoChildren, message, setLatestMessage, conversation?.conversationId]);

View file

@ -1,28 +1,28 @@
import type { TMessage } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider'; import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
type TUseGenerations = { type TUseGenerations = {
error?: boolean;
endpoint?: string; endpoint?: string;
message?: TMessage; messageId?: string;
isSubmitting: boolean;
isEditing?: boolean; isEditing?: boolean;
latestMessage: TMessage | null; isSubmitting: boolean;
searchResult?: boolean;
finish_reason?: string;
latestMessageId?: string;
isCreatedByUser?: boolean;
}; };
export default function useGenerationsByLatest({ export default function useGenerationsByLatest({
error = false,
endpoint, endpoint,
message, messageId,
isSubmitting,
isEditing = false, isEditing = false,
latestMessage, isSubmitting,
searchResult = false,
finish_reason = '',
latestMessageId,
isCreatedByUser = false,
}: TUseGenerations) { }: TUseGenerations) {
const {
messageId,
searchResult = false,
error = false,
finish_reason = '',
isCreatedByUser = false,
} = message ?? {};
const isEditableEndpoint = Boolean( const isEditableEndpoint = Boolean(
[ [
EModelEndpoint.openAI, EModelEndpoint.openAI,
@ -37,7 +37,7 @@ export default function useGenerationsByLatest({
); );
const continueSupported = const continueSupported =
latestMessage?.messageId === messageId && latestMessageId === messageId &&
finish_reason && finish_reason &&
finish_reason !== 'stop' && finish_reason !== 'stop' &&
!isEditing && !isEditing &&

View file

@ -204,9 +204,9 @@ export function getIconEndpoint({
iconURL, iconURL,
endpoint, endpoint,
}: { }: {
endpointsConfig: t.TEndpointsConfig | undefined; endpointsConfig?: t.TEndpointsConfig;
iconURL: string | undefined; iconURL?: string | null;
endpoint: string | null | undefined; endpoint?: string | null;
}) { }) {
return (endpointsConfig?.[iconURL ?? ''] ? iconURL ?? endpoint : endpoint) ?? ''; return (endpointsConfig?.[iconURL ?? ''] ? iconURL ?? endpoint : endpoint) ?? '';
} }
@ -219,7 +219,7 @@ export function getIconKey({
endpointIconURL: iconURL, endpointIconURL: iconURL,
}: { }: {
endpoint?: string | null; endpoint?: string | null;
endpointsConfig?: t.TEndpointsConfig | undefined; endpointsConfig?: t.TEndpointsConfig;
endpointType?: string | null; endpointType?: string | null;
endpointIconURL?: string; endpointIconURL?: string;
}) { }) {

View file

@ -475,7 +475,7 @@ export const tMessageSchema = z.object({
/* assistant */ /* assistant */
thread_id: z.string().optional(), thread_id: z.string().optional(),
/* frontend components */ /* frontend components */
iconURL: z.string().optional(), iconURL: z.string().nullable().optional(),
}); });
export type TAttachmentMetadata = { messageId: string; toolCallId: string }; export type TAttachmentMetadata = { messageId: string; toolCallId: string };