mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🎭 refactor: Avatar Loading UX and Fix Initials Rendering Bugs (#9261)
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
e559f0f4dc
commit
94426a3cae
9 changed files with 562 additions and 31 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@librechat/client",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"description": "React components for LibreChat",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
|
@ -64,7 +64,9 @@
|
|||
"react-hook-form": "^7.56.4",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"tailwind-merge": "^1.9.1"
|
||||
"tailwind-merge": "^1.9.1",
|
||||
"@dicebear/core": "^9.2.2",
|
||||
"@dicebear/collection": "^9.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
|
|
|
|||
102
packages/client/src/components/Avatar.tsx
Normal file
102
packages/client/src/components/Avatar.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
import { Skeleton } from './Skeleton';
|
||||
import { useAvatar } from '~/hooks';
|
||||
import { UserIcon } from '~/svgs';
|
||||
|
||||
export interface AvatarProps {
|
||||
user?: TUser;
|
||||
size?: number;
|
||||
className?: string;
|
||||
alt?: string;
|
||||
showDefaultWhenEmpty?: boolean;
|
||||
}
|
||||
|
||||
const Avatar: React.FC<AvatarProps> = ({
|
||||
user,
|
||||
size = 32,
|
||||
className = '',
|
||||
alt,
|
||||
showDefaultWhenEmpty = true,
|
||||
}) => {
|
||||
const avatarSrc = useAvatar(user);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
const avatarSeed = useMemo(
|
||||
() => user?.avatar || user?.username || user?.email || '',
|
||||
[user?.avatar, user?.username, user?.email],
|
||||
);
|
||||
|
||||
const altText = useMemo(
|
||||
() => alt || `${user?.name || user?.username || user?.email || ''}'s avatar`,
|
||||
[alt, user?.name, user?.username, user?.email],
|
||||
);
|
||||
|
||||
const imageSrc = useMemo(() => {
|
||||
if (!avatarSeed || imageError) return '';
|
||||
return (user?.avatar ?? '') || avatarSrc || '';
|
||||
}, [user?.avatar, avatarSrc, avatarSeed, imageError]);
|
||||
|
||||
const handleImageLoad = useCallback(() => {
|
||||
setImageLoaded(true);
|
||||
}, []);
|
||||
|
||||
const handleImageError = useCallback(() => {
|
||||
setImageError(true);
|
||||
setImageLoaded(false);
|
||||
}, []);
|
||||
|
||||
const DefaultAvatar = useCallback(
|
||||
() => (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgb(121, 137, 255)',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
|
||||
}}
|
||||
className={`relative flex items-center justify-center rounded-full p-1 text-text-primary ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<UserIcon />
|
||||
</div>
|
||||
),
|
||||
[size, className],
|
||||
);
|
||||
|
||||
if (avatarSeed.length === 0 && showDefaultWhenEmpty) {
|
||||
return <DefaultAvatar />;
|
||||
}
|
||||
|
||||
if (avatarSeed.length > 0 && !imageError) {
|
||||
return (
|
||||
<div className="relative" style={{ width: `${size}px`, height: `${size}px` }}>
|
||||
{!imageLoaded && (
|
||||
<Skeleton className="rounded-full" style={{ width: `${size}px`, height: `${size}px` }} />
|
||||
)}
|
||||
|
||||
<img
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
display: imageLoaded ? 'block' : 'none',
|
||||
}}
|
||||
className={`rounded-full ${className}`}
|
||||
src={imageSrc}
|
||||
alt={altText}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (imageError && showDefaultWhenEmpty) {
|
||||
return <DefaultAvatar />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
|
|
@ -33,6 +33,7 @@ export * from './Resizable';
|
|||
export * from './Select';
|
||||
export { default as Radio } from './Radio';
|
||||
export { default as Badge } from './Badge';
|
||||
export { default as Avatar } from './Avatar';
|
||||
export { default as Combobox } from './Combobox';
|
||||
export { default as Dropdown } from './Dropdown';
|
||||
export { default as SplitText } from './SplitText';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
export type { TranslationKeys } from './useLocalize';
|
||||
|
||||
export { default as useToast } from './useToast';
|
||||
export { default as useAvatar } from './useAvatar';
|
||||
export { default as useCombobox } from './useCombobox';
|
||||
export { default as useLocalize } from './useLocalize';
|
||||
export { default as useMediaQuery } from './useMediaQuery';
|
||||
|
|
|
|||
44
packages/client/src/hooks/useAvatar.ts
Normal file
44
packages/client/src/hooks/useAvatar.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { useMemo } from 'react';
|
||||
import { createAvatar } from '@dicebear/core';
|
||||
import { initials } from '@dicebear/collection';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
|
||||
const avatarCache: Record<string, string> = {};
|
||||
|
||||
const useAvatar = (user: TUser | undefined) => {
|
||||
return useMemo(() => {
|
||||
const { username, name } = user ?? {};
|
||||
const seed = name || username;
|
||||
if (!seed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (user?.avatar && user?.avatar !== '') {
|
||||
return user.avatar;
|
||||
}
|
||||
|
||||
if (avatarCache[seed]) {
|
||||
return avatarCache[seed];
|
||||
}
|
||||
|
||||
const avatar = createAvatar(initials, {
|
||||
seed,
|
||||
fontFamily: ['Verdana'],
|
||||
fontSize: 36,
|
||||
});
|
||||
|
||||
let avatarDataUri = '';
|
||||
try {
|
||||
avatarDataUri = avatar.toDataUri();
|
||||
if (avatarDataUri) {
|
||||
avatarCache[seed] = avatarDataUri;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate avatar:', error);
|
||||
}
|
||||
|
||||
return avatarDataUri;
|
||||
}, [user]);
|
||||
};
|
||||
|
||||
export default useAvatar;
|
||||
Loading…
Add table
Add a link
Reference in a new issue