This commit is contained in:
Marco Beretta 2025-12-15 10:32:09 -05:00 committed by GitHub
commit fc4a797b2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 71 additions and 12 deletions

View file

@ -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
<EndpointIcon
conversation={conversation}
endpointsConfig={endpointsConfig}
agentsMap={agentsMap}
size={20}
context="menu-item"
/>

View file

@ -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 (

View file

@ -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 (
<div className={className} style={containerStyle}>
<div className={`${className} relative`} style={containerStyle}>
<img
src={iconURL}
alt={altName ?? 'Icon'}
style={imageStyle}
style={{
...imageStyle,
opacity: isLoaded ? 1 : 0,
transition: 'opacity 0.2s ease-in-out',
}}
className="object-cover"
onLoad={() => setIsLoaded(true)}
onError={handleImageError}
loading="lazy"
decoding="async"
width={Number(containerStyle.width) || 20}
height={Number(containerStyle.height) || 20}
/>
{!isLoaded && !imageError && (
<Skeleton
className="absolute inset-0 rounded-full"
style={{ width: containerStyle.width, height: containerStyle.height }}
aria-hidden="true"
/>
)}
</div>
);
},

View file

@ -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 && <Skeleton className="absolute inset-0 rounded-full" aria-hidden="true" />}
{!isLoaded && !hasError && (
<Skeleton className="absolute inset-0 rounded-full" aria-hidden="true" />
)}
</>
);
};