mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
Merge 0cf71b9e18 into 02fc4647e1
This commit is contained in:
commit
fc4a797b2c
4 changed files with 71 additions and 12 deletions
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue