diff --git a/client/src/components/Chat/Input/AddedConvo.tsx b/client/src/components/Chat/Input/AddedConvo.tsx index 63152693ce..764612ec90 100644 --- a/client/src/components/Chat/Input/AddedConvo.tsx +++ b/client/src/components/Chat/Input/AddedConvo.tsx @@ -41,6 +41,7 @@ export default function AddedConvo({ + ); +} + +export default memo(ConversationEndpointIcon, (prevProps, nextProps) => { + return ( + prevProps.className === nextProps.className && + prevProps.context === nextProps.context && + prevProps.size === nextProps.size && + areConversationIconFieldsEqual(prevProps.conversation, nextProps.conversation) + ); +}); diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx index 9412208809..26c670341a 100644 --- a/client/src/components/Conversations/Conversations.tsx +++ b/client/src/components/Conversations/Conversations.tsx @@ -1,11 +1,11 @@ import { useMemo, memo, type FC, useCallback, useEffect, useRef } from 'react'; import throttle from 'lodash/throttle'; -import { ChevronDown } from 'lucide-react'; import { useRecoilValue } from 'recoil'; -import { useQueryClient } from '@tanstack/react-query'; +import { ChevronDown } from 'lucide-react'; import { QueryKeys } from 'librechat-data-provider'; -import { Spinner, TooltipAnchor, NewChatIcon, useMediaQuery } from '@librechat/client'; +import { useQueryClient } from '@tanstack/react-query'; import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized'; +import { Spinner, TooltipAnchor, NewChatIcon, useMediaQuery } from '@librechat/client'; import type { TConversation } from 'librechat-data-provider'; import { useLocalize, @@ -14,9 +14,9 @@ import { useShowMarketplace, useNewConvo, } from '~/hooks'; +import { groupConversationsByDate, clearMessagesCache, cn } from '~/utils'; import FavoritesList from '~/components/Nav/Favorites/FavoritesList'; import { useActiveJobs } from '~/data-provider'; -import { groupConversationsByDate, clearMessagesCache, cn } from '~/utils'; import Convo from './Convo'; import store from '~/store'; @@ -160,38 +160,6 @@ type FlattenedItem = | { type: 'convo'; convo: TConversation } | { type: 'loading' }; -const MemoizedConvo = memo( - ({ - conversation, - retainView, - toggleNav, - isGenerating, - }: { - conversation: TConversation; - retainView: () => void; - toggleNav: () => void; - isGenerating: boolean; - }) => { - return ( - - ); - }, - (prevProps, nextProps) => { - return ( - prevProps.conversation.conversationId === nextProps.conversation.conversationId && - prevProps.conversation.title === nextProps.conversation.title && - prevProps.conversation.endpoint === nextProps.conversation.endpoint && - prevProps.conversation.chatProjectId === nextProps.conversation.chatProjectId && - prevProps.isGenerating === nextProps.isGenerating - ); - }, -); - const Conversations: FC = ({ conversations: rawConversations, moveToTop, @@ -352,7 +320,7 @@ const Conversations: FC = ({ const isGenerating = activeJobIds.has(item.convo.conversationId ?? ''); return ( - params.conversationId, [params.conversationId]); const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? ''); const activeConvos = useRecoilValue(store.allConversationsSelector); @@ -177,6 +176,42 @@ export default function Conversation({ isShiftHeld: isActiveConvo ? isShiftHeld : false, }; + const generatingSpinner = ( + + + + + ); + + let actionVisibilityClassName = + 'pointer-events-none max-w-0 scale-x-0 opacity-0 group-focus-within:pointer-events-auto group-focus-within:max-w-[60px] group-focus-within:scale-x-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:max-w-[60px] group-hover:scale-x-100 group-hover:opacity-100'; + if (isGenerating) { + actionVisibilityClassName = 'pointer-events-none w-5 scale-x-100 opacity-100'; + } else if (isPopoverActive || isActiveConvo) { + actionVisibilityClassName = 'pointer-events-auto scale-x-100 opacity-100'; + } + + let actionWidthClassName = ''; + if (!isGenerating && !isPopoverActive && isActiveConvo && isShiftHeld) { + actionWidthClassName = 'max-w-[60px]'; + } else if (!isGenerating) { + actionWidthClassName = 'max-w-[28px]'; + } + + const showConvoOptions = !renaming && (hasInteracted || isActiveConvo); + const actionContent = isGenerating + ? generatingSpinner + : showConvoOptions && ; + return (
- {isGenerating ? ( - - - - - ) : ( - - )} + )}
); } + +export default memo(Conversation, areConversationRenderPropsEqual); diff --git a/client/src/components/Conversations/ConvoLink.tsx b/client/src/components/Conversations/ConvoLink.tsx index 543407501e..6fd251f02b 100644 --- a/client/src/components/Conversations/ConvoLink.tsx +++ b/client/src/components/Conversations/ConvoLink.tsx @@ -23,7 +23,7 @@ const ConvoLink: React.FC = ({ return (
{}; -function ProjectChatsInline({ - projectId, - toggleNav, - onShowAll, -}: { +type ProjectChatsInlineProps = { projectId: string; toggleNav: () => void; onShowAll: () => void; -}) { +}; + +const ProjectChatsInline = memo(function ProjectChatsInline({ + projectId, + toggleNav, + onShowAll, +}: ProjectChatsInlineProps) { const localize = useLocalize(); + const { data: activeJobsData } = useActiveJobs(); + const activeJobIds = useMemo( + () => new Set(activeJobsData?.activeJobIds ?? []), + [activeJobsData?.activeJobIds], + ); const { data, isLoading } = useConversationsInfiniteQuery( { projectId, sortBy: 'updatedAt', sortDirection: 'desc' }, { staleTime: 30000, cacheTime: 300000 }, @@ -235,7 +243,7 @@ function ProjectChatsInline({ conversation={convo} retainView={noop} toggleNav={toggleNav} - isGenerating={false} + isGenerating={activeJobIds.has(convo.conversationId ?? '')} /> ))} {hasMore && ( @@ -249,130 +257,152 @@ function ProjectChatsInline({ )}
); -} +}); -function ProjectItem({ - project, - toggleNav, - defaultExpanded, -}: { +ProjectChatsInline.displayName = 'ProjectChatsInline'; + +type ProjectItemProps = { project: TChatProject; toggleNav: () => void; defaultExpanded: boolean; -}) { - const localize = useLocalize(); - const navigate = useNavigate(); - const queryClient = useQueryClient(); - const { newConversation } = useNewConvo(); - const conversation = useRecoilValue(store.conversationByIndex(0)); - const menuId = useId(); - const [expanded, setExpanded] = useState(defaultExpanded); - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isRenameOpen, setIsRenameOpen] = useState(false); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); +}; - const openProject = useCallback(() => { - navigate(`/projects/${project._id}`); - toggleNav(); - }, [navigate, project._id, toggleNav]); +const ProjectItem = memo( + function ProjectItem({ project, toggleNav, defaultExpanded }: ProjectItemProps) { + const localize = useLocalize(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const { newConversation } = useNewConvo(); + const getCurrentConversationId = useRecoilCallback( + ({ snapshot }) => + async () => { + const conversation = await snapshot.getPromise(store.conversationByIndex(0)); + return conversation?.conversationId; + }, + [], + ); + const menuId = useId(); + const [expanded, setExpanded] = useState(defaultExpanded); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isRenameOpen, setIsRenameOpen] = useState(false); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); - const startChat = useCallback(() => { - clearMessagesCache(queryClient, conversation?.conversationId); - queryClient.invalidateQueries([QueryKeys.messages]); - newConversation({ template: { chatProjectId: project._id } }); - toggleNav(); - }, [conversation?.conversationId, newConversation, project._id, queryClient, toggleNav]); + const openProject = useCallback(() => { + navigate(`/projects/${project._id}`); + toggleNav(); + }, [navigate, project._id, toggleNav]); - const menuItems = useMemo( - () => [ - { - id: `${menuId}-open`, - label: localize('com_ui_open_project'), - icon: