diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index c10aad33e3..3aea1d0441 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -7,6 +7,7 @@ import type { TConversation } from 'librechat-data-provider'; import { useUpdateConversationMutation } from '~/data-provider'; import EndpointIcon from '~/components/Endpoints/EndpointIcon'; import { useNavigateToConvo, useLocalize, useShiftKey } from '~/hooks'; +import { useAgentsMapContext } from '~/Providers/AgentsMapContext'; import { useGetEndpointsQuery } from '~/data-provider'; import { NotificationSeverity } from '~/common'; import { ConvoOptions } from './ConvoOptions'; @@ -25,6 +26,7 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co const params = useParams(); const localize = useLocalize(); const { showToast } = useToastContext(); + const agentsMap = useAgentsMapContext(); const { navigateToConvo } = useNavigateToConvo(); const { data: endpointsConfig } = useGetEndpointsQuery(); const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]); @@ -186,6 +188,7 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co diff --git a/client/src/components/Endpoints/EndpointIcon.tsx b/client/src/components/Endpoints/EndpointIcon.tsx index c32ea12369..9e4f2bb6a7 100644 --- a/client/src/components/Endpoints/EndpointIcon.tsx +++ b/client/src/components/Endpoints/EndpointIcon.tsx @@ -1,19 +1,21 @@ -import { getEndpointField, isAssistantsEndpoint } from 'librechat-data-provider'; +import { getEndpointField, isAssistantsEndpoint, EModelEndpoint } from 'librechat-data-provider'; import type { TPreset, + TAgentsMap, TConversation, TAssistantsMap, TEndpointsConfig, } from 'librechat-data-provider'; import ConvoIconURL from '~/components/Endpoints/ConvoIconURL'; import MinimalIcon from '~/components/Endpoints/MinimalIcon'; -import { getIconEndpoint } from '~/utils'; +import { getIconEndpoint, getAgentAvatarUrl } from '~/utils'; export default function EndpointIcon({ conversation, endpointsConfig, className = 'mr-0', assistantMap, + agentsMap, context, }: { conversation: TConversation | TPreset | null; @@ -21,6 +23,7 @@ export default function EndpointIcon({ containerClassName?: string; context?: 'message' | 'nav' | 'landing' | 'menu-item'; assistantMap?: TAssistantsMap; + agentsMap?: TAgentsMap; className?: string; size?: number; }) { @@ -37,7 +40,11 @@ export default function EndpointIcon({ const assistantAvatar = (assistant && (assistant.metadata?.avatar as string)) || ''; const assistantName = assistant && (assistant.name ?? ''); - const iconURL = assistantAvatar || convoIconURL; + const agent = + endpoint === EModelEndpoint.agents ? agentsMap?.[conversation?.agent_id ?? ''] : null; + const agentAvatar = getAgentAvatarUrl(agent) ?? ''; + + const iconURL = assistantAvatar || agentAvatar || convoIconURL; if (iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))) { return ( diff --git a/client/src/components/Endpoints/URLIcon.tsx b/client/src/components/Endpoints/URLIcon.tsx index ddb62aaded..366289a6e4 100644 --- a/client/src/components/Endpoints/URLIcon.tsx +++ b/client/src/components/Endpoints/URLIcon.tsx @@ -1,6 +1,8 @@ -import React, { memo, useState } from 'react'; +import React, { memo, useState, useEffect } from 'react'; import { AlertCircle } from 'lucide-react'; +import { Skeleton } from '@librechat/client'; import { icons } from '~/hooks/Endpoint/Icons'; +import { isImageCached } from '~/utils'; export const URLIcon = memo( ({ @@ -19,9 +21,21 @@ export const URLIcon = memo( endpoint?: string; }) => { const [imageError, setImageError] = useState(false); + const [isLoaded, setIsLoaded] = useState(() => isImageCached(iconURL)); + + useEffect(() => { + if (isImageCached(iconURL)) { + setIsLoaded(true); + setImageError(false); + } else { + setIsLoaded(false); + setImageError(false); + } + }, [iconURL]); const handleImageError = () => { setImageError(true); + setIsLoaded(false); }; const DefaultIcon: React.ElementType = @@ -46,18 +60,29 @@ export const URLIcon = memo( } return ( -
+
{altName setIsLoaded(true)} onError={handleImageError} - loading="lazy" decoding="async" width={Number(containerStyle.width) || 20} height={Number(containerStyle.height) || 20} /> + {!isLoaded && !imageError && ( +
); }, diff --git a/client/src/utils/agents.tsx b/client/src/utils/agents.tsx index e83a94c1aa..a65577d1f3 100644 --- a/client/src/utils/agents.tsx +++ b/client/src/utils/agents.tsx @@ -3,6 +3,19 @@ import { Feather } from 'lucide-react'; import { Skeleton } from '@librechat/client'; import type t from 'librechat-data-provider'; +/** + * Checks if an image is already cached in the browser + * Returns true if image is complete and has valid dimensions + */ +export const isImageCached = (url: string | null | undefined): boolean => { + if (typeof window === 'undefined' || !url) { + return false; + } + const img = new Image(); + img.src = url; + return img.complete && img.naturalWidth > 0; +}; + /** * Extracts the avatar URL from an agent's avatar property * Handles both string and object formats @@ -32,10 +45,17 @@ const LazyAgentAvatar = ({ alt: string; imgClass: string; }) => { - const [isLoaded, setIsLoaded] = useState(false); + const [isLoaded, setIsLoaded] = useState(() => isImageCached(url)); + const [hasError, setHasError] = useState(false); useEffect(() => { - setIsLoaded(false); + if (isImageCached(url)) { + setIsLoaded(true); + setHasError(false); + } else { + setIsLoaded(false); + setHasError(false); + } }, [url]); return ( @@ -44,15 +64,19 @@ const LazyAgentAvatar = ({ src={url} alt={alt} className={imgClass} - loading="lazy" onLoad={() => setIsLoaded(true)} - onError={() => setIsLoaded(false)} + onError={() => { + setIsLoaded(false); + setHasError(true); + }} style={{ opacity: isLoaded ? 1 : 0, transition: 'opacity 0.2s ease-in-out', }} /> - {!isLoaded &&