mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50: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 { useUpdateConversationMutation } from '~/data-provider';
|
||||||
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
|
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
|
||||||
import { useNavigateToConvo, useLocalize, useShiftKey } from '~/hooks';
|
import { useNavigateToConvo, useLocalize, useShiftKey } from '~/hooks';
|
||||||
|
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
|
||||||
import { useGetEndpointsQuery } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
import { ConvoOptions } from './ConvoOptions';
|
import { ConvoOptions } from './ConvoOptions';
|
||||||
|
|
@ -25,6 +26,7 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
const agentsMap = useAgentsMapContext();
|
||||||
const { navigateToConvo } = useNavigateToConvo();
|
const { navigateToConvo } = useNavigateToConvo();
|
||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||||
const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]);
|
const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]);
|
||||||
|
|
@ -186,6 +188,7 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
||||||
<EndpointIcon
|
<EndpointIcon
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
endpointsConfig={endpointsConfig}
|
endpointsConfig={endpointsConfig}
|
||||||
|
agentsMap={agentsMap}
|
||||||
size={20}
|
size={20}
|
||||||
context="menu-item"
|
context="menu-item"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
import { getEndpointField, isAssistantsEndpoint } from 'librechat-data-provider';
|
import { getEndpointField, isAssistantsEndpoint, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type {
|
import type {
|
||||||
TPreset,
|
TPreset,
|
||||||
|
TAgentsMap,
|
||||||
TConversation,
|
TConversation,
|
||||||
TAssistantsMap,
|
TAssistantsMap,
|
||||||
TEndpointsConfig,
|
TEndpointsConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||||
import MinimalIcon from '~/components/Endpoints/MinimalIcon';
|
import MinimalIcon from '~/components/Endpoints/MinimalIcon';
|
||||||
import { getIconEndpoint } from '~/utils';
|
import { getIconEndpoint, getAgentAvatarUrl } from '~/utils';
|
||||||
|
|
||||||
export default function EndpointIcon({
|
export default function EndpointIcon({
|
||||||
conversation,
|
conversation,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
className = 'mr-0',
|
className = 'mr-0',
|
||||||
assistantMap,
|
assistantMap,
|
||||||
|
agentsMap,
|
||||||
context,
|
context,
|
||||||
}: {
|
}: {
|
||||||
conversation: TConversation | TPreset | null;
|
conversation: TConversation | TPreset | null;
|
||||||
|
|
@ -21,6 +23,7 @@ export default function EndpointIcon({
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
context?: 'message' | 'nav' | 'landing' | 'menu-item';
|
context?: 'message' | 'nav' | 'landing' | 'menu-item';
|
||||||
assistantMap?: TAssistantsMap;
|
assistantMap?: TAssistantsMap;
|
||||||
|
agentsMap?: TAgentsMap;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -37,7 +40,11 @@ export default function EndpointIcon({
|
||||||
const assistantAvatar = (assistant && (assistant.metadata?.avatar as string)) || '';
|
const assistantAvatar = (assistant && (assistant.metadata?.avatar as string)) || '';
|
||||||
const assistantName = assistant && (assistant.name ?? '');
|
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/'))) {
|
if (iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { memo, useState } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
import { Skeleton } from '@librechat/client';
|
||||||
import { icons } from '~/hooks/Endpoint/Icons';
|
import { icons } from '~/hooks/Endpoint/Icons';
|
||||||
|
import { isImageCached } from '~/utils';
|
||||||
|
|
||||||
export const URLIcon = memo(
|
export const URLIcon = memo(
|
||||||
({
|
({
|
||||||
|
|
@ -19,9 +21,21 @@ export const URLIcon = memo(
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [imageError, setImageError] = useState(false);
|
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 = () => {
|
const handleImageError = () => {
|
||||||
setImageError(true);
|
setImageError(true);
|
||||||
|
setIsLoaded(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DefaultIcon: React.ElementType =
|
const DefaultIcon: React.ElementType =
|
||||||
|
|
@ -46,18 +60,29 @@ export const URLIcon = memo(
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} style={containerStyle}>
|
<div className={`${className} relative`} style={containerStyle}>
|
||||||
<img
|
<img
|
||||||
src={iconURL}
|
src={iconURL}
|
||||||
alt={altName ?? 'Icon'}
|
alt={altName ?? 'Icon'}
|
||||||
style={imageStyle}
|
style={{
|
||||||
|
...imageStyle,
|
||||||
|
opacity: isLoaded ? 1 : 0,
|
||||||
|
transition: 'opacity 0.2s ease-in-out',
|
||||||
|
}}
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
|
onLoad={() => setIsLoaded(true)}
|
||||||
onError={handleImageError}
|
onError={handleImageError}
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
decoding="async"
|
||||||
width={Number(containerStyle.width) || 20}
|
width={Number(containerStyle.width) || 20}
|
||||||
height={Number(containerStyle.height) || 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>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,19 @@ import { Feather } from 'lucide-react';
|
||||||
import { Skeleton } from '@librechat/client';
|
import { Skeleton } from '@librechat/client';
|
||||||
import type t from 'librechat-data-provider';
|
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
|
* Extracts the avatar URL from an agent's avatar property
|
||||||
* Handles both string and object formats
|
* Handles both string and object formats
|
||||||
|
|
@ -32,10 +45,17 @@ const LazyAgentAvatar = ({
|
||||||
alt: string;
|
alt: string;
|
||||||
imgClass: string;
|
imgClass: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(() => isImageCached(url));
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isImageCached(url)) {
|
||||||
|
setIsLoaded(true);
|
||||||
|
setHasError(false);
|
||||||
|
} else {
|
||||||
setIsLoaded(false);
|
setIsLoaded(false);
|
||||||
|
setHasError(false);
|
||||||
|
}
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -44,15 +64,19 @@ const LazyAgentAvatar = ({
|
||||||
src={url}
|
src={url}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className={imgClass}
|
className={imgClass}
|
||||||
loading="lazy"
|
|
||||||
onLoad={() => setIsLoaded(true)}
|
onLoad={() => setIsLoaded(true)}
|
||||||
onError={() => setIsLoaded(false)}
|
onError={() => {
|
||||||
|
setIsLoaded(false);
|
||||||
|
setHasError(true);
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
opacity: isLoaded ? 1 : 0,
|
opacity: isLoaded ? 1 : 0,
|
||||||
transition: 'opacity 0.2s ease-in-out',
|
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