feat: Enhance image loading and error handling in URLIcon and LazyAgentAvatar components

This commit is contained in:
Marco Beretta 2025-12-12 22:45:41 +01:00
parent 087ffa972e
commit 96ea589881
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 68 additions and 9 deletions

View file

@ -1,5 +1,6 @@
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';
export const URLIcon = memo( export const URLIcon = memo(
@ -19,9 +20,34 @@ export const URLIcon = memo(
endpoint?: string; endpoint?: string;
}) => { }) => {
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
const [isLoaded, setIsLoaded] = useState(() => {
// Check if image is already cached
if (typeof window !== 'undefined' && iconURL) {
const img = new Image();
img.src = iconURL;
return img.complete && img.naturalWidth > 0;
}
return false;
});
useEffect(() => {
// When URL changes, check if new image is cached
if (iconURL) {
const img = new Image();
img.src = iconURL;
if (img.complete && img.naturalWidth > 0) {
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 +72,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>
); );
}, },

View file

@ -32,10 +32,28 @@ const LazyAgentAvatar = ({
alt: string; alt: string;
imgClass: string; imgClass: string;
}) => { }) => {
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(() => {
// Check if image is already cached by creating a test image
if (typeof window !== 'undefined') {
const img = new Image();
img.src = url;
return img.complete && img.naturalWidth > 0;
}
return false;
});
const [hasError, setHasError] = useState(false);
useEffect(() => { useEffect(() => {
// When URL changes, check if new image is cached
const img = new Image();
img.src = url;
if (img.complete && img.naturalWidth > 0) {
setIsLoaded(true);
setHasError(false);
} else {
setIsLoaded(false); setIsLoaded(false);
setHasError(false);
}
}, [url]); }, [url]);
return ( return (
@ -44,15 +62,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" />
)}
</> </>
); );
}; };