From 06719794f66fac3406e4f382d53a1e197250af93 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 12 Dec 2025 23:09:05 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=9D=EF=B8=8F=20fix:=20React=20Key=20Pr?= =?UTF-8?q?ops=20and=20Minor=20UI=20Fixes=20from=20a11y=20Updates=20(#1095?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Update Frontend logger function to enhance logging conditions - Modified the logger function to check for logger enablement and development environment more robustly. - Adjusted the condition to ensure logging occurs only when the logger is enabled or when the environment variable for logger is not set in development mode. * fix: Add key prop to MeasuredRow components in Conversations for improved rendering - Updated MeasuredRow components to include a key prop for better performance and to prevent rendering issues during list updates. - Ensured consistent handling of item types within the Conversations component. * refactor: Enhance ScrollToBottom component with forwardRef for improved functionality - Updated ScrollToBottom component to use forwardRef, allowing it to accept a ref for better integration with parent components. - Modified MessagesView to utilize the new ref for the ScrollToBottom button, improving scrolling behavior and performance. * refactor: Enhance EndpointItem and renderEndpoints for improved model render keys - Updated EndpointItem to accept an endpointIndex prop for better indexing of endpoints. - Modified renderEndpoints to pass the endpointIndex to EndpointItem, improving the rendering of endpoint models. - Adjusted renderEndpointModels to utilize the endpointIndex for unique key generation, enhancing performance and preventing rendering issues. * refactor: Update BaseClient to handle non-ephemeral agents in conversation logic - Added a check for non-ephemeral agents in BaseClient, modifying the exceptions set to include 'model' when applicable. - Enhanced conversation handling to improve flexibility based on agent type. * refactor: Optimize FavoritesList component for agent handling and loading states - Updated FavoritesList to improve agent ID management by introducing combinedAgentsMap for better handling of missing agents. - Refactored loading state logic to ensure accurate representation of agent loading status. - Enhanced the use of useQueries for fetching missing agent data, streamlining the overall data retrieval process. - Improved memoization of agent IDs and loading conditions for better performance and reliability. * Revert "refactor: Update BaseClient to handle non-ephemeral agents in conversation logic" This reverts commit 6738acbe041a08d7c15d09e6cf5c3fde036640f6. --- .../Endpoints/components/EndpointItem.tsx | 28 +++++-- .../components/EndpointModelItem.tsx | 7 +- .../components/Chat/Messages/MessagesView.tsx | 6 +- .../Conversations/Conversations.tsx | 10 +-- .../components/Messages/ScrollToBottom.tsx | 11 ++- .../Nav/Favorites/FavoritesList.tsx | 82 +++++++++---------- client/src/utils/logger.ts | 2 +- 7 files changed, 84 insertions(+), 62 deletions(-) diff --git a/client/src/components/Chat/Menus/Endpoints/components/EndpointItem.tsx b/client/src/components/Chat/Menus/Endpoints/components/EndpointItem.tsx index c21c3a8e4e..1679d0285a 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/EndpointItem.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/EndpointItem.tsx @@ -14,6 +14,7 @@ import { cn } from '~/utils'; interface EndpointItemProps { endpoint: Endpoint; + endpointIndex: number; } const SettingsButton = ({ @@ -54,7 +55,7 @@ const SettingsButton = ({ ); }; -export function EndpointItem({ endpoint }: EndpointItemProps) { +export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) { const localize = useLocalize(); const { agentsMap, @@ -153,8 +154,21 @@ export function EndpointItem({ endpoint }: EndpointItemProps) { ))} {/* Render endpoint models */} {filteredModels - ? renderEndpointModels(endpoint, endpoint.models || [], selectedModel, filteredModels) - : endpoint.models && renderEndpointModels(endpoint, endpoint.models, selectedModel)} + ? renderEndpointModels( + endpoint, + endpoint.models || [], + selectedModel, + filteredModels, + endpointIndex, + ) + : endpoint.models && + renderEndpointModels( + endpoint, + endpoint.models, + selectedModel, + undefined, + endpointIndex, + )} )} @@ -198,7 +212,11 @@ export function EndpointItem({ endpoint }: EndpointItemProps) { } export function renderEndpoints(mappedEndpoints: Endpoint[]) { - return mappedEndpoints.map((endpoint) => ( - + return mappedEndpoints.map((endpoint, index) => ( + )); } diff --git a/client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx b/client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx index aab5b5889f..cb9d24eb61 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx @@ -109,7 +109,6 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod return ( handleSelectModel(endpoint, modelId ?? '')} className="group flex w-full cursor-pointer items-center justify-between rounded-lg px-2 text-sm" > @@ -161,14 +160,16 @@ export function renderEndpointModels( models: Array<{ name: string; isGlobal?: boolean }>, selectedModel: string | null, filteredModels?: string[], + endpointIndex?: number, ) { const modelsToRender = filteredModels || models.map((model) => model.name); + const indexSuffix = endpointIndex != null ? `-${endpointIndex}` : ''; return modelsToRender.map( - (modelId) => + (modelId, modelIndex) => endpoint && ( (-1); + const scrollToBottomRef = useRef(null); const { conversation, @@ -87,8 +88,9 @@ function MessagesViewContent({ classNames="scroll-animation" unmountOnExit={true} appear={true} + nodeRef={scrollToBottomRef} > - + diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx index 63ee52ee9b..64b804b2d6 100644 --- a/client/src/components/Conversations/Conversations.tsx +++ b/client/src/components/Conversations/Conversations.tsx @@ -250,7 +250,7 @@ const Conversations: FC = ({ if (item.type === 'loading') { return ( - + ); @@ -258,7 +258,7 @@ const Conversations: FC = ({ if (item.type === 'favorites') { return ( - + = ({ if (item.type === 'chats-header') { return ( - + setIsChatsExpanded(!isChatsExpanded)} @@ -285,7 +285,7 @@ const Conversations: FC = ({ // Without favorites: [chats-header, first-header] → index 1 const firstHeaderIndex = shouldShowFavorites ? 2 : 1; return ( - + ); @@ -293,7 +293,7 @@ const Conversations: FC = ({ if (item.type === 'convo') { return ( - + ); diff --git a/client/src/components/Messages/ScrollToBottom.tsx b/client/src/components/Messages/ScrollToBottom.tsx index bb67cfe3df..0b99df0a61 100644 --- a/client/src/components/Messages/ScrollToBottom.tsx +++ b/client/src/components/Messages/ScrollToBottom.tsx @@ -1,12 +1,13 @@ -import React from 'react'; +import { forwardRef } from 'react'; type Props = { scrollHandler: React.MouseEventHandler; }; -export default function ScrollToBottom({ scrollHandler }: Props) { +const ScrollToBottom = forwardRef(({ scrollHandler }, ref) => { return ( ); -} +}); + +ScrollToBottom.displayName = 'ScrollToBottom'; + +export default ScrollToBottom; diff --git a/client/src/components/Nav/Favorites/FavoritesList.tsx b/client/src/components/Nav/Favorites/FavoritesList.tsx index b8af6a99b1..84c0283602 100644 --- a/client/src/components/Nav/Favorites/FavoritesList.tsx +++ b/client/src/components/Nav/Favorites/FavoritesList.tsx @@ -4,14 +4,13 @@ import { LayoutGrid } from 'lucide-react'; import { useDrag, useDrop } from 'react-dnd'; import { Skeleton } from '@librechat/client'; import { useNavigate } from 'react-router-dom'; +import { useQueries } from '@tanstack/react-query'; import { QueryKeys, dataService } from 'librechat-data-provider'; -import { useQueries, useQueryClient } from '@tanstack/react-query'; -import type { InfiniteData } from '@tanstack/react-query'; import type t from 'librechat-data-provider'; import { useFavorites, useLocalize, useShowMarketplace, useNewConvo } from '~/hooks'; +import { useAssistantsMapContext, useAgentsMapContext } from '~/Providers'; import useSelectMention from '~/hooks/Input/useSelectMention'; import { useGetEndpointsQuery } from '~/data-provider'; -import { useAssistantsMapContext } from '~/Providers'; import FavoriteItem from './FavoriteItem'; import store from '~/store'; @@ -121,13 +120,13 @@ export default function FavoritesList({ }) { const navigate = useNavigate(); const localize = useLocalize(); - const queryClient = useQueryClient(); const search = useRecoilValue(store.search); const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites(); const showAgentMarketplace = useShowMarketplace(); const { newConversation } = useNewConvo(); const assistantsMap = useAssistantsMapContext(); + const agentsMap = useAgentsMapContext(); const conversation = useRecoilValue(store.conversationByIndex(0)); const { data: endpointsConfig = {} as t.TEndpointsConfig } = useGetEndpointsQuery(); @@ -168,59 +167,56 @@ export default function FavoritesList({ newChatButton?.focus(); }, []); - // Ensure favorites is always an array (could be corrupted in localStorage) const safeFavorites = useMemo(() => (Array.isArray(favorites) ? favorites : []), [favorites]); - const agentIds = safeFavorites.map((f) => f.agentId).filter(Boolean) as string[]; + const allAgentIds = useMemo( + () => safeFavorites.map((f) => f.agentId).filter(Boolean) as string[], + [safeFavorites], + ); - const agentQueries = useQueries({ - queries: agentIds.map((agentId) => ({ + const missingAgentIds = useMemo(() => { + if (agentsMap === undefined) { + return []; + } + return allAgentIds.filter((id) => !agentsMap[id]); + }, [allAgentIds, agentsMap]); + + const missingAgentQueries = useQueries({ + queries: missingAgentIds.map((agentId) => ({ queryKey: [QueryKeys.agent, agentId], queryFn: () => dataService.getAgentById({ agent_id: agentId }), staleTime: 1000 * 60 * 5, + enabled: missingAgentIds.length > 0, })), }); - const isAgentsLoading = agentIds.length > 0 && agentQueries.some((q) => q.isLoading); + const combinedAgentsMap = useMemo(() => { + if (agentsMap === undefined) { + return undefined; + } + const combined: Record = {}; + for (const [key, value] of Object.entries(agentsMap)) { + if (value) { + combined[key] = value; + } + } + missingAgentQueries.forEach((query) => { + if (query.data) { + combined[query.data.id] = query.data; + } + }); + return combined; + }, [agentsMap, missingAgentQueries]); + + const isAgentsLoading = + (allAgentIds.length > 0 && agentsMap === undefined) || + (missingAgentIds.length > 0 && missingAgentQueries.some((q) => q.isLoading)); useEffect(() => { if (!isAgentsLoading && onHeightChange) { onHeightChange(); } }, [isAgentsLoading, onHeightChange]); - const agentsMap = useMemo(() => { - const map: Record = {}; - - const addToMap = (agent: t.Agent) => { - if (agent && agent.id && !map[agent.id]) { - map[agent.id] = agent; - } - }; - - const marketplaceData = queryClient.getQueriesData>([ - QueryKeys.marketplaceAgents, - ]); - marketplaceData.forEach(([_, data]) => { - data?.pages.forEach((page) => { - page.data.forEach(addToMap); - }); - }); - - const agentsListData = queryClient.getQueriesData([QueryKeys.agents]); - agentsListData.forEach(([_, data]) => { - if (data && Array.isArray(data.data)) { - data.data.forEach(addToMap); - } - }); - - agentQueries.forEach((query) => { - if (query.data) { - map[query.data.id] = query.data; - } - }); - - return map; - }, [agentQueries, queryClient]); const draggedFavoritesRef = useRef(safeFavorites); @@ -306,7 +302,7 @@ export default function FavoritesList({ )} {safeFavorites.map((fav, index) => { if (fav.agentId) { - const agent = agentsMap[fav.agentId]; + const agent = combinedAgentsMap?.[fav.agentId]; if (!agent) { return null; } diff --git a/client/src/utils/logger.ts b/client/src/utils/logger.ts index 6bc1d21db6..b025f4926c 100644 --- a/client/src/utils/logger.ts +++ b/client/src/utils/logger.ts @@ -9,7 +9,7 @@ const createLogFunction = ( type?: 'log' | 'warn' | 'error' | 'info' | 'debug' | 'dir', ): LogFunction => { return (...args: unknown[]) => { - if (isDevelopment || isLoggerEnabled) { + if (isLoggerEnabled || (import.meta.env.VITE_ENABLE_LOGGER == null && isDevelopment)) { const tag = typeof args[0] === 'string' ? args[0] : ''; if (shouldLog(tag)) { if (tag && typeof args[1] === 'string' && type === 'error') {