mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🗝️ fix: React Key Props and Minor UI Fixes from a11y Updates (#10954)
* 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 6738acbe04.
This commit is contained in:
parent
4d7e6b4a58
commit
06719794f6
7 changed files with 84 additions and 62 deletions
|
|
@ -14,6 +14,7 @@ import { cn } from '~/utils';
|
||||||
|
|
||||||
interface EndpointItemProps {
|
interface EndpointItemProps {
|
||||||
endpoint: Endpoint;
|
endpoint: Endpoint;
|
||||||
|
endpointIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsButton = ({
|
const SettingsButton = ({
|
||||||
|
|
@ -54,7 +55,7 @@ const SettingsButton = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function EndpointItem({ endpoint }: EndpointItemProps) {
|
export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const {
|
const {
|
||||||
agentsMap,
|
agentsMap,
|
||||||
|
|
@ -153,8 +154,21 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
|
||||||
))}
|
))}
|
||||||
{/* Render endpoint models */}
|
{/* Render endpoint models */}
|
||||||
{filteredModels
|
{filteredModels
|
||||||
? renderEndpointModels(endpoint, endpoint.models || [], selectedModel, filteredModels)
|
? renderEndpointModels(
|
||||||
: endpoint.models && renderEndpointModels(endpoint, endpoint.models, selectedModel)}
|
endpoint,
|
||||||
|
endpoint.models || [],
|
||||||
|
selectedModel,
|
||||||
|
filteredModels,
|
||||||
|
endpointIndex,
|
||||||
|
)
|
||||||
|
: endpoint.models &&
|
||||||
|
renderEndpointModels(
|
||||||
|
endpoint,
|
||||||
|
endpoint.models,
|
||||||
|
selectedModel,
|
||||||
|
undefined,
|
||||||
|
endpointIndex,
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
@ -198,7 +212,11 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderEndpoints(mappedEndpoints: Endpoint[]) {
|
export function renderEndpoints(mappedEndpoints: Endpoint[]) {
|
||||||
return mappedEndpoints.map((endpoint) => (
|
return mappedEndpoints.map((endpoint, index) => (
|
||||||
<EndpointItem endpoint={endpoint} key={`endpoint-${endpoint.value}-item`} />
|
<EndpointItem
|
||||||
|
endpoint={endpoint}
|
||||||
|
endpointIndex={index}
|
||||||
|
key={`endpoint-${endpoint.value}-${index}`}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,6 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
key={modelId}
|
|
||||||
onClick={() => handleSelectModel(endpoint, modelId ?? '')}
|
onClick={() => handleSelectModel(endpoint, modelId ?? '')}
|
||||||
className="group flex w-full cursor-pointer items-center justify-between rounded-lg px-2 text-sm"
|
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 }>,
|
models: Array<{ name: string; isGlobal?: boolean }>,
|
||||||
selectedModel: string | null,
|
selectedModel: string | null,
|
||||||
filteredModels?: string[],
|
filteredModels?: string[],
|
||||||
|
endpointIndex?: number,
|
||||||
) {
|
) {
|
||||||
const modelsToRender = filteredModels || models.map((model) => model.name);
|
const modelsToRender = filteredModels || models.map((model) => model.name);
|
||||||
|
const indexSuffix = endpointIndex != null ? `-${endpointIndex}` : '';
|
||||||
|
|
||||||
return modelsToRender.map(
|
return modelsToRender.map(
|
||||||
(modelId) =>
|
(modelId, modelIndex) =>
|
||||||
endpoint && (
|
endpoint && (
|
||||||
<EndpointModelItem
|
<EndpointModelItem
|
||||||
key={modelId}
|
key={`${endpoint.value}${indexSuffix}-${modelId}-${modelIndex}`}
|
||||||
modelId={modelId}
|
modelId={modelId}
|
||||||
endpoint={endpoint}
|
endpoint={endpoint}
|
||||||
isSelected={selectedModel === modelId}
|
isSelected={selectedModel === modelId}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
|
|
@ -21,6 +21,7 @@ function MessagesViewContent({
|
||||||
const { screenshotTargetRef } = useScreenshot();
|
const { screenshotTargetRef } = useScreenshot();
|
||||||
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
|
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
|
||||||
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
||||||
|
const scrollToBottomRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
conversation,
|
conversation,
|
||||||
|
|
@ -87,8 +88,9 @@ function MessagesViewContent({
|
||||||
classNames="scroll-animation"
|
classNames="scroll-animation"
|
||||||
unmountOnExit={true}
|
unmountOnExit={true}
|
||||||
appear={true}
|
appear={true}
|
||||||
|
nodeRef={scrollToBottomRef}
|
||||||
>
|
>
|
||||||
<ScrollToBottom scrollHandler={handleSmoothToRef} />
|
<ScrollToBottom ref={scrollToBottomRef} scrollHandler={handleSmoothToRef} />
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
|
|
||||||
if (item.type === 'loading') {
|
if (item.type === 'loading') {
|
||||||
return (
|
return (
|
||||||
<MeasuredRow {...rowProps}>
|
<MeasuredRow key={key} {...rowProps}>
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</MeasuredRow>
|
</MeasuredRow>
|
||||||
);
|
);
|
||||||
|
|
@ -258,7 +258,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
|
|
||||||
if (item.type === 'favorites') {
|
if (item.type === 'favorites') {
|
||||||
return (
|
return (
|
||||||
<MeasuredRow {...rowProps}>
|
<MeasuredRow key={key} {...rowProps}>
|
||||||
<FavoritesList
|
<FavoritesList
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
toggleNav={toggleNav}
|
toggleNav={toggleNav}
|
||||||
|
|
@ -270,7 +270,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
|
|
||||||
if (item.type === 'chats-header') {
|
if (item.type === 'chats-header') {
|
||||||
return (
|
return (
|
||||||
<MeasuredRow {...rowProps}>
|
<MeasuredRow key={key} {...rowProps}>
|
||||||
<ChatsHeader
|
<ChatsHeader
|
||||||
isExpanded={isChatsExpanded}
|
isExpanded={isChatsExpanded}
|
||||||
onToggle={() => setIsChatsExpanded(!isChatsExpanded)}
|
onToggle={() => setIsChatsExpanded(!isChatsExpanded)}
|
||||||
|
|
@ -285,7 +285,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
// Without favorites: [chats-header, first-header] → index 1
|
// Without favorites: [chats-header, first-header] → index 1
|
||||||
const firstHeaderIndex = shouldShowFavorites ? 2 : 1;
|
const firstHeaderIndex = shouldShowFavorites ? 2 : 1;
|
||||||
return (
|
return (
|
||||||
<MeasuredRow {...rowProps}>
|
<MeasuredRow key={key} {...rowProps}>
|
||||||
<DateLabel groupName={item.groupName} isFirst={index === firstHeaderIndex} />
|
<DateLabel groupName={item.groupName} isFirst={index === firstHeaderIndex} />
|
||||||
</MeasuredRow>
|
</MeasuredRow>
|
||||||
);
|
);
|
||||||
|
|
@ -293,7 +293,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
|
|
||||||
if (item.type === 'convo') {
|
if (item.type === 'convo') {
|
||||||
return (
|
return (
|
||||||
<MeasuredRow {...rowProps}>
|
<MeasuredRow key={key} {...rowProps}>
|
||||||
<MemoizedConvo conversation={item.convo} retainView={moveToTop} toggleNav={toggleNav} />
|
<MemoizedConvo conversation={item.convo} retainView={moveToTop} toggleNav={toggleNav} />
|
||||||
</MeasuredRow>
|
</MeasuredRow>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
scrollHandler: React.MouseEventHandler<HTMLButtonElement>;
|
scrollHandler: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ScrollToBottom({ scrollHandler }: Props) {
|
const ScrollToBottom = forwardRef<HTMLButtonElement, Props>(({ scrollHandler }, ref) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
ref={ref}
|
||||||
onClick={scrollHandler}
|
onClick={scrollHandler}
|
||||||
className="premium-scroll-button absolute bottom-5 right-1/2 cursor-pointer border border-border-light bg-surface-secondary"
|
className="premium-scroll-button absolute bottom-5 right-1/2 cursor-pointer border border-border-light bg-surface-secondary"
|
||||||
aria-label="Scroll to bottom"
|
aria-label="Scroll to bottom"
|
||||||
|
|
@ -22,4 +23,8 @@ export default function ScrollToBottom({ scrollHandler }: Props) {
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
ScrollToBottom.displayName = 'ScrollToBottom';
|
||||||
|
|
||||||
|
export default ScrollToBottom;
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,13 @@ import { LayoutGrid } from 'lucide-react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { Skeleton } from '@librechat/client';
|
import { Skeleton } from '@librechat/client';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useQueries } from '@tanstack/react-query';
|
||||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
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 type t from 'librechat-data-provider';
|
||||||
import { useFavorites, useLocalize, useShowMarketplace, useNewConvo } from '~/hooks';
|
import { useFavorites, useLocalize, useShowMarketplace, useNewConvo } from '~/hooks';
|
||||||
|
import { useAssistantsMapContext, useAgentsMapContext } from '~/Providers';
|
||||||
import useSelectMention from '~/hooks/Input/useSelectMention';
|
import useSelectMention from '~/hooks/Input/useSelectMention';
|
||||||
import { useGetEndpointsQuery } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
import { useAssistantsMapContext } from '~/Providers';
|
|
||||||
import FavoriteItem from './FavoriteItem';
|
import FavoriteItem from './FavoriteItem';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -121,13 +120,13 @@ export default function FavoritesList({
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const search = useRecoilValue(store.search);
|
const search = useRecoilValue(store.search);
|
||||||
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
|
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
|
||||||
const showAgentMarketplace = useShowMarketplace();
|
const showAgentMarketplace = useShowMarketplace();
|
||||||
|
|
||||||
const { newConversation } = useNewConvo();
|
const { newConversation } = useNewConvo();
|
||||||
const assistantsMap = useAssistantsMapContext();
|
const assistantsMap = useAssistantsMapContext();
|
||||||
|
const agentsMap = useAgentsMapContext();
|
||||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||||
const { data: endpointsConfig = {} as t.TEndpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig = {} as t.TEndpointsConfig } = useGetEndpointsQuery();
|
||||||
|
|
||||||
|
|
@ -168,59 +167,56 @@ export default function FavoritesList({
|
||||||
newChatButton?.focus();
|
newChatButton?.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Ensure favorites is always an array (could be corrupted in localStorage)
|
|
||||||
const safeFavorites = useMemo(() => (Array.isArray(favorites) ? favorites : []), [favorites]);
|
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({
|
const missingAgentIds = useMemo(() => {
|
||||||
queries: agentIds.map((agentId) => ({
|
if (agentsMap === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return allAgentIds.filter((id) => !agentsMap[id]);
|
||||||
|
}, [allAgentIds, agentsMap]);
|
||||||
|
|
||||||
|
const missingAgentQueries = useQueries({
|
||||||
|
queries: missingAgentIds.map((agentId) => ({
|
||||||
queryKey: [QueryKeys.agent, agentId],
|
queryKey: [QueryKeys.agent, agentId],
|
||||||
queryFn: () => dataService.getAgentById({ agent_id: agentId }),
|
queryFn: () => dataService.getAgentById({ agent_id: agentId }),
|
||||||
staleTime: 1000 * 60 * 5,
|
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<string, t.Agent> = {};
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isAgentsLoading && onHeightChange) {
|
if (!isAgentsLoading && onHeightChange) {
|
||||||
onHeightChange();
|
onHeightChange();
|
||||||
}
|
}
|
||||||
}, [isAgentsLoading, onHeightChange]);
|
}, [isAgentsLoading, onHeightChange]);
|
||||||
const agentsMap = useMemo(() => {
|
|
||||||
const map: Record<string, t.Agent> = {};
|
|
||||||
|
|
||||||
const addToMap = (agent: t.Agent) => {
|
|
||||||
if (agent && agent.id && !map[agent.id]) {
|
|
||||||
map[agent.id] = agent;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const marketplaceData = queryClient.getQueriesData<InfiniteData<t.AgentListResponse>>([
|
|
||||||
QueryKeys.marketplaceAgents,
|
|
||||||
]);
|
|
||||||
marketplaceData.forEach(([_, data]) => {
|
|
||||||
data?.pages.forEach((page) => {
|
|
||||||
page.data.forEach(addToMap);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const agentsListData = queryClient.getQueriesData<t.AgentListResponse>([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);
|
const draggedFavoritesRef = useRef(safeFavorites);
|
||||||
|
|
||||||
|
|
@ -306,7 +302,7 @@ export default function FavoritesList({
|
||||||
)}
|
)}
|
||||||
{safeFavorites.map((fav, index) => {
|
{safeFavorites.map((fav, index) => {
|
||||||
if (fav.agentId) {
|
if (fav.agentId) {
|
||||||
const agent = agentsMap[fav.agentId];
|
const agent = combinedAgentsMap?.[fav.agentId];
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const createLogFunction = (
|
||||||
type?: 'log' | 'warn' | 'error' | 'info' | 'debug' | 'dir',
|
type?: 'log' | 'warn' | 'error' | 'info' | 'debug' | 'dir',
|
||||||
): LogFunction => {
|
): LogFunction => {
|
||||||
return (...args: unknown[]) => {
|
return (...args: unknown[]) => {
|
||||||
if (isDevelopment || isLoggerEnabled) {
|
if (isLoggerEnabled || (import.meta.env.VITE_ENABLE_LOGGER == null && isDevelopment)) {
|
||||||
const tag = typeof args[0] === 'string' ? args[0] : '';
|
const tag = typeof args[0] === 'string' ? args[0] : '';
|
||||||
if (shouldLog(tag)) {
|
if (shouldLog(tag)) {
|
||||||
if (tag && typeof args[1] === 'string' && type === 'error') {
|
if (tag && typeof args[1] === 'string' && type === 'error') {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue