diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 0d9e43e196..24f74b5197 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -1182,7 +1182,15 @@ ${convo} } let UnexpectedRoleError = false; + /** @type {Promise} */ + let streamPromise; + /** @type {(value: void | PromiseLike) => void} */ + let streamResolve; + if (modelOptions.stream) { + streamPromise = new Promise((resolve) => { + streamResolve = resolve; + }); const stream = await openai.beta.chat.completions .stream({ ...modelOptions, @@ -1194,13 +1202,17 @@ ${convo} .on('error', (err) => { handleOpenAIErrors(err, errorCallback, 'stream'); }) - .on('finalChatCompletion', (finalChatCompletion) => { + .on('finalChatCompletion', async (finalChatCompletion) => { const finalMessage = finalChatCompletion?.choices?.[0]?.message; - if (finalMessage && finalMessage?.role !== 'assistant') { + if (!finalMessage) { + return; + } + await streamPromise; + if (finalMessage?.role !== 'assistant') { finalChatCompletion.choices[0].message.role = 'assistant'; } - if (finalMessage && !finalMessage?.content?.trim()) { + if (typeof finalMessage.content !== 'string' || finalMessage.content.trim() === '') { finalChatCompletion.choices[0].message.content = intermediateReply; } }) @@ -1223,6 +1235,8 @@ ${convo} await sleep(streamRate); } + streamResolve(); + if (!UnexpectedRoleError) { chatCompletion = await stream.finalChatCompletion().catch((err) => { handleOpenAIErrors(err, errorCallback, 'finalChatCompletion'); diff --git a/client/src/components/Chat/Messages/Message.tsx b/client/src/components/Chat/Messages/Message.tsx index 6cd0611221..3f7705a6d6 100644 --- a/client/src/components/Chat/Messages/Message.tsx +++ b/client/src/components/Chat/Messages/Message.tsx @@ -6,7 +6,13 @@ import MessageRender from './ui/MessageRender'; import MultiMessage from './MultiMessage'; const MessageContainer = React.memo( - ({ handleScroll, children }: { handleScroll: () => void; children: React.ReactNode }) => { + ({ + handleScroll, + children, + }: { + handleScroll: (event?: unknown) => void; + children: React.ReactNode; + }) => { return (
diff --git a/client/src/components/Chat/Messages/MessageIcon.tsx b/client/src/components/Chat/Messages/MessageIcon.tsx index 3e09259434..ce09b6ca26 100644 --- a/client/src/components/Chat/Messages/MessageIcon.tsx +++ b/client/src/components/Chat/Messages/MessageIcon.tsx @@ -1,4 +1,4 @@ -import { useMemo, memo } from 'react'; +import React, { useMemo, memo } from 'react'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { TMessage, TPreset, Assistant } from 'librechat-data-provider'; import type { TMessageProps } from '~/common'; @@ -6,55 +6,66 @@ import ConvoIconURL from '~/components/Endpoints/ConvoIconURL'; import { getEndpointField, getIconEndpoint } from '~/utils'; import Icon from '~/components/Endpoints/Icon'; -function MessageIcon( - props: Pick & { - assistant?: Assistant; - }, -) { - const { data: endpointsConfig } = useGetEndpointsQuery(); - const { message, conversation, assistant } = props; +const MessageIcon = memo( + ( + props: Pick & { + assistant?: Assistant; + }, + ) => { + const { data: endpointsConfig } = useGetEndpointsQuery(); + const { message, conversation, assistant } = props; - const assistantName = assistant ? (assistant.name as string | undefined) : ''; - const assistantAvatar = assistant ? (assistant.metadata?.avatar as string | undefined) : ''; + const assistantName = useMemo(() => assistant?.name ?? '', [assistant]); + const assistantAvatar = useMemo(() => assistant?.metadata?.avatar ?? '', [assistant]); + const isCreatedByUser = useMemo(() => message?.isCreatedByUser ?? false, [message]); - const messageSettings = useMemo( - () => ({ - ...(conversation ?? {}), - ...({ - ...(message ?? {}), - iconURL: message?.iconURL ?? '', - } as TMessage), - }), - [conversation, message], - ); + const messageSettings = useMemo( + () => ({ + ...(conversation ?? {}), + ...({ + ...(message ?? {}), + iconURL: message?.iconURL ?? '', + } as TMessage), + }), + [conversation, message], + ); - const iconURL = messageSettings.iconURL; - let endpoint = messageSettings.endpoint; - endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint }); - const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL'); + const iconURL = messageSettings.iconURL; + const endpoint = useMemo( + () => getIconEndpoint({ endpointsConfig, iconURL, endpoint: messageSettings.endpoint }), + [endpointsConfig, iconURL, messageSettings.endpoint], + ); + + const endpointIconURL = useMemo( + () => getEndpointField(endpointsConfig, endpoint, 'iconURL'), + [endpointsConfig, endpoint], + ); + + if (isCreatedByUser !== true && iconURL != null && iconURL.includes('http')) { + return ( + + ); + } - if (message?.isCreatedByUser !== true && iconURL != null && iconURL.includes('http')) { return ( - ); - } + }, +); - return ( - - ); -} +MessageIcon.displayName = 'MessageIcon'; -export default memo(MessageIcon); +export default MessageIcon; diff --git a/client/src/components/Endpoints/ConvoIconURL.tsx b/client/src/components/Endpoints/ConvoIconURL.tsx index 8501ed0261..f3753d08d9 100644 --- a/client/src/components/Endpoints/ConvoIconURL.tsx +++ b/client/src/components/Endpoints/ConvoIconURL.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { memo } from 'react'; import type { TPreset } from 'librechat-data-provider'; import type { IconMapProps } from '~/common'; import { icons } from '~/components/Chat/Menus/Endpoints/Icons'; @@ -41,7 +41,7 @@ const ConvoIconURL: React.FC = ({ }, ) => React.JSX.Element; - const isURL = iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/')); + const isURL = !!(iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))); if (!isURL) { Icon = icons[iconURL] ?? icons.unknown; @@ -77,4 +77,4 @@ const ConvoIconURL: React.FC = ({ ); }; -export default ConvoIconURL; +export default memo(ConvoIconURL); diff --git a/client/src/components/Endpoints/Icon.tsx b/client/src/components/Endpoints/Icon.tsx index 0059725a1e..4ae8db12b8 100644 --- a/client/src/components/Endpoints/Icon.tsx +++ b/client/src/components/Endpoints/Icon.tsx @@ -1,4 +1,5 @@ -import { memo } from 'react'; +import React, { memo } from 'react'; +import type { TUser } from 'librechat-data-provider'; import type { IconProps } from '~/common'; import MessageEndpointIcon from './MessageEndpointIcon'; import { useAuthContext } from '~/hooks/AuthContext'; @@ -7,7 +8,44 @@ import useLocalize from '~/hooks/useLocalize'; import { UserIcon } from '~/components/svg'; import { cn } from '~/utils'; -const Icon: React.FC = (props) => { +type UserAvatarProps = { + size: number; + user?: TUser; + avatarSrc: string; + username: string; + className?: string; +}; + +const UserAvatar = memo(({ size, user, avatarSrc, username, className }: UserAvatarProps) => ( +
+ {!(user?.avatar ?? '') && (!(user?.username ?? '') || user?.username.trim() === '') ? ( +
+ +
+ ) : ( + avatar + )} +
+)); + +UserAvatar.displayName = 'UserAvatar'; + +const Icon: React.FC = memo((props) => { const { user } = useAuthContext(); const { size = 30, isCreatedByUser } = props; @@ -15,36 +53,20 @@ const Icon: React.FC = (props) => { const localize = useLocalize(); if (isCreatedByUser) { - const username = user?.name || user?.username || localize('com_nav_user'); - + const username = user?.name ?? user?.username ?? localize('com_nav_user'); return ( -
- {!user?.avatar && !user?.username ? ( -
- -
- ) : ( - avatar - )} -
+ ); } return ; -}; +}); -export default memo(Icon); +Icon.displayName = 'Icon'; + +export default Icon; diff --git a/client/src/components/Prompts/Groups/ChatGroupItem.tsx b/client/src/components/Prompts/Groups/ChatGroupItem.tsx index 68bf01dd69..2ac6fbcfda 100644 --- a/client/src/components/Prompts/Groups/ChatGroupItem.tsx +++ b/client/src/components/Prompts/Groups/ChatGroupItem.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, memo } from 'react'; import { Menu as MenuIcon, Edit as EditIcon, EarthIcon, TextSearch } from 'lucide-react'; import type { TPromptGroup } from 'librechat-data-provider'; import { @@ -14,7 +14,7 @@ import PreviewPrompt from '~/components/Prompts/PreviewPrompt'; import ListCard from '~/components/Prompts/Groups/ListCard'; import { detectVariables } from '~/utils'; -export default function ChatGroupItem({ +function ChatGroupItem({ group, instanceProjectId, }: { @@ -116,3 +116,5 @@ export default function ChatGroupItem({ ); } + +export default memo(ChatGroupItem); diff --git a/client/src/components/ui/ControlCombobox.tsx b/client/src/components/ui/ControlCombobox.tsx index abd8d69700..da42fee16e 100644 --- a/client/src/components/ui/ControlCombobox.tsx +++ b/client/src/components/ui/ControlCombobox.tsx @@ -1,6 +1,6 @@ import * as Ariakit from '@ariakit/react'; import { matchSorter } from 'match-sorter'; -import { startTransition, useMemo, useState, useEffect, useRef } from 'react'; +import { startTransition, useMemo, useState, useEffect, useRef, memo } from 'react'; import { cn } from '~/utils'; import type { OptionWithIcon } from '~/common'; import { Search } from 'lucide-react'; @@ -17,7 +17,7 @@ interface ControlComboboxProps { SelectIcon?: React.ReactNode; } -export default function ControlCombobox({ +function ControlCombobox({ selectedValue, displayValue, items, @@ -121,3 +121,5 @@ export default function ControlCombobox({
); } + +export default memo(ControlCombobox); diff --git a/client/src/components/ui/DropdownPopup.tsx b/client/src/components/ui/DropdownPopup.tsx index e8aa63ba4c..3a64321d73 100644 --- a/client/src/components/ui/DropdownPopup.tsx +++ b/client/src/components/ui/DropdownPopup.tsx @@ -37,6 +37,10 @@ const DropdownPopup: React.FC = ({ cannot appear as a descendant of