🎭 refactor: Avatar Loading UX and Fix Initials Rendering Bugs (#9261)

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2025-08-25 18:06:00 +02:00 committed by GitHub
parent e559f0f4dc
commit 94426a3cae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 562 additions and 31 deletions

View file

@ -1,10 +1,9 @@
import React, { memo, useState } from 'react'; import React, { memo, useState } from 'react';
import { UserIcon } from '@librechat/client'; import { UserIcon, useAvatar } from '@librechat/client';
import type { TUser } from 'librechat-data-provider'; import type { TUser } from 'librechat-data-provider';
import type { IconProps } from '~/common'; import type { IconProps } from '~/common';
import MessageEndpointIcon from './MessageEndpointIcon'; import MessageEndpointIcon from './MessageEndpointIcon';
import { useAuthContext } from '~/hooks/AuthContext'; import { useAuthContext } from '~/hooks/AuthContext';
import useAvatar from '~/hooks/Messages/useAvatar';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';

View file

@ -2,11 +2,10 @@ import { useState, memo } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import * as Select from '@ariakit/react/select'; import * as Select from '@ariakit/react/select';
import { FileText, LogOut } from 'lucide-react'; import { FileText, LogOut } from 'lucide-react';
import { LinkIcon, GearIcon, DropdownMenuSeparator, UserIcon } from '@librechat/client'; import { LinkIcon, GearIcon, DropdownMenuSeparator, Avatar } from '@librechat/client';
import { useGetStartupConfig, useGetUserBalance } from '~/data-provider'; import { useGetStartupConfig, useGetUserBalance } from '~/data-provider';
import FilesView from '~/components/Chat/Input/Files/FilesView'; import FilesView from '~/components/Chat/Input/Files/FilesView';
import { useAuthContext } from '~/hooks/AuthContext'; import { useAuthContext } from '~/hooks/AuthContext';
import useAvatar from '~/hooks/Messages/useAvatar';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import Settings from './Settings'; import Settings from './Settings';
import store from '~/store'; import store from '~/store';
@ -21,9 +20,6 @@ function AccountSettings() {
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [showFiles, setShowFiles] = useRecoilState(store.showFiles); const [showFiles, setShowFiles] = useRecoilState(store.showFiles);
const avatarSrc = useAvatar(user);
const avatarSeed = user?.avatar || user?.name || user?.username || '';
return ( return (
<Select.SelectProvider> <Select.SelectProvider>
<Select.Select <Select.Select
@ -33,26 +29,7 @@ function AccountSettings() {
> >
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0"> <div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
<div className="relative flex"> <div className="relative flex">
{avatarSeed.length === 0 ? ( <Avatar user={user} size={32} />
<div
style={{
backgroundColor: 'rgb(121, 137, 255)',
width: '32px',
height: '32px',
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"
aria-hidden="true"
>
<UserIcon />
</div>
) : (
<img
className="rounded-full"
src={(user?.avatar ?? '') || avatarSrc}
alt={`${user?.name || user?.username || user?.email || ''}'s avatar`}
/>
)}
</div> </div>
</div> </div>
<div <div

View file

@ -1,4 +1,3 @@
export { default as useAvatar } from './useAvatar';
export { default as useProgress } from './useProgress'; export { default as useProgress } from './useProgress';
export { default as useAttachments } from './useAttachments'; export { default as useAttachments } from './useAttachments';
export { default as useSubmitMessage } from './useSubmitMessage'; export { default as useSubmitMessage } from './useSubmitMessage';

452
package-lock.json generated
View file

@ -17654,6 +17654,454 @@
"kuler": "^2.0.0" "kuler": "^2.0.0"
} }
}, },
"node_modules/@dicebear/adventurer": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.2.4.tgz",
"integrity": "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/adventurer-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.2.4.tgz",
"integrity": "sha512-I9IrB4ZYbUHSOUpWoUbfX3vG8FrjcW8htoQ4bEOR7TYOKKE11Mo1nrGMuHZ7GPfwN0CQeK1YVJhWqLTmtYn7Pg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/avataaars": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.2.4.tgz",
"integrity": "sha512-QKNBtA/1QGEzR+JjS4XQyrFHYGbzdOp0oa6gjhGhUDrMegDFS8uyjdRfDQsFTebVkyLWjgBQKZEiDqKqHptB6A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/avataaars-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.2.4.tgz",
"integrity": "sha512-HtBvA7elRv50QTOOsBdtYB1GVimCpGEDlDgWsu1snL5Z3d1+3dIESoXQd3mXVvKTVT8Z9ciA4TEaF09WfxDjAA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/big-ears": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.2.4.tgz",
"integrity": "sha512-U33tbh7Io6wG6ViUMN5fkWPER7hPKMaPPaYgafaYQlCT4E7QPKF2u8X1XGag3jCKm0uf4SLXfuZ8v+YONcHmNQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/big-ears-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.2.4.tgz",
"integrity": "sha512-pPjYu80zMFl43A9sa5+tAKPkhp4n9nd7eN878IOrA1HAowh/XePh5JN8PTkNFS9eM+rnN9m8WX08XYFe30kLYw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/big-smile": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.2.4.tgz",
"integrity": "sha512-zeEfXOOXy7j9tfkPLzfQdLBPyQsctBetTdEfKRArc1k3RUliNPxfJG9j88+cXQC6GXrVW2pcT2X50NSPtugCFQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/bottts": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.2.4.tgz",
"integrity": "sha512-4CTqrnVg+NQm6lZ4UuCJish8gGWe8EqSJrzvHQRO5TEyAKjYxbTdVqejpkycG1xkawha4FfxsYgtlSx7UwoVMw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/bottts-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.2.4.tgz",
"integrity": "sha512-eMVdofdD/udHsKIaeWEXShDRtiwk7vp4FjY7l0f79vIzfhkIsXKEhPcnvHKOl/yoArlDVS3Uhgjj0crWTO9RJA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/collection": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.2.4.tgz",
"integrity": "sha512-I1wCUp0yu5qSIeMQHmDYXQIXKkKjcja/SYBxppPkYFXpR2alxb0k9/swFDdMbkY6a1c9AT1kI1y+Pg6ywQ2rTA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dicebear/adventurer": "9.2.4",
"@dicebear/adventurer-neutral": "9.2.4",
"@dicebear/avataaars": "9.2.4",
"@dicebear/avataaars-neutral": "9.2.4",
"@dicebear/big-ears": "9.2.4",
"@dicebear/big-ears-neutral": "9.2.4",
"@dicebear/big-smile": "9.2.4",
"@dicebear/bottts": "9.2.4",
"@dicebear/bottts-neutral": "9.2.4",
"@dicebear/croodles": "9.2.4",
"@dicebear/croodles-neutral": "9.2.4",
"@dicebear/dylan": "9.2.4",
"@dicebear/fun-emoji": "9.2.4",
"@dicebear/glass": "9.2.4",
"@dicebear/icons": "9.2.4",
"@dicebear/identicon": "9.2.4",
"@dicebear/initials": "9.2.4",
"@dicebear/lorelei": "9.2.4",
"@dicebear/lorelei-neutral": "9.2.4",
"@dicebear/micah": "9.2.4",
"@dicebear/miniavs": "9.2.4",
"@dicebear/notionists": "9.2.4",
"@dicebear/notionists-neutral": "9.2.4",
"@dicebear/open-peeps": "9.2.4",
"@dicebear/personas": "9.2.4",
"@dicebear/pixel-art": "9.2.4",
"@dicebear/pixel-art-neutral": "9.2.4",
"@dicebear/rings": "9.2.4",
"@dicebear/shapes": "9.2.4",
"@dicebear/thumbs": "9.2.4"
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/core": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.2.4.tgz",
"integrity": "sha512-hz6zArEcUwkZzGOSJkWICrvqnEZY7BKeiq9rqKzVJIc1tRVv0MkR0FGvIxSvXiK9TTIgKwu656xCWAGAl6oh+w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/json-schema": "^7.0.11"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@dicebear/croodles": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.2.4.tgz",
"integrity": "sha512-CqT0NgVfm+5kd+VnjGY4WECNFeOrj5p7GCPTSEA7tCuN72dMQOX47P9KioD3wbExXYrIlJgOcxNrQeb/FMGc3A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/croodles-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.2.4.tgz",
"integrity": "sha512-8vAS9lIEKffSUVx256GSRAlisB8oMX38UcPWw72venO/nitLVsyZ6hZ3V7eBdII0Onrjqw1RDndslQODbVcpTw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/dylan": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.2.4.tgz",
"integrity": "sha512-tiih1358djAq0jDDzmW3N3S4C3ynC2yn4hhlTAq/MaUAQtAi47QxdHdFGdxH0HBMZKqA4ThLdVk3yVgN4xsukg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/fun-emoji": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.2.4.tgz",
"integrity": "sha512-Od729skczse1HvHekgEFv+mSuJKMC4sl5hENGi/izYNe6DZDqJrrD0trkGT/IVh/SLXUFbq1ZFY9I2LoUGzFZg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/glass": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.2.4.tgz",
"integrity": "sha512-5lxbJode1t99eoIIgW0iwZMoZU4jNMJv/6vbsgYUhAslYFX5zP0jVRscksFuo89TTtS7YKqRqZAL3eNhz4bTDw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/icons": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.2.4.tgz",
"integrity": "sha512-bRsK1qj8u9Z76xs8XhXlgVr/oHh68tsHTJ/1xtkX9DeTQTSamo2tS26+r231IHu+oW3mePtFnwzdG9LqEPRd4A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/identicon": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.2.4.tgz",
"integrity": "sha512-R9nw/E8fbu9HltHOqI9iL/o9i7zM+2QauXWMreQyERc39oGR9qXiwgBxsfYGcIS4C85xPyuL5B3I2RXrLBlJPg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/initials": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.2.4.tgz",
"integrity": "sha512-4SzHG5WoQZl1TGcpEZR4bdsSkUVqwNQCOwWSPAoBJa3BNxbVsvL08LF7I97BMgrCoknWZjQHUYt05amwTPTKtg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/lorelei": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.2.4.tgz",
"integrity": "sha512-eS4mPYUgDpo89HvyFAx/kgqSSKh8W4zlUA8QJeIUCWTB0WpQmeqkSgIyUJjGDYSrIujWi+zEhhckksM5EwW0Dg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/lorelei-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.2.4.tgz",
"integrity": "sha512-bWq2/GonbcJULtT+B/MGcM2UnA7kBQoH+INw8/oW83WI3GNTZ6qEwe3/W4QnCgtSOhUsuwuiSULguAFyvtkOZQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/micah": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.2.4.tgz",
"integrity": "sha512-XNWJ8Mx+pncIV8Ye0XYc/VkMiax8kTxcP3hLTC5vmELQyMSLXzg/9SdpI+W/tCQghtPZRYTT3JdY9oU9IUlP2g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/miniavs": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.2.4.tgz",
"integrity": "sha512-k7IYTAHE/4jSO6boMBRrNlqPT3bh7PLFM1atfe0nOeCDwmz/qJUBP3HdONajbf3fmo8f2IZYhELrNWTOE7Ox3Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/notionists": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.2.4.tgz",
"integrity": "sha512-zcvpAJ93EfC0xQffaPZQuJPShwPhnu9aTcoPsaYGmw0oEDLcv2XYmDhUUdX84QYCn6LtCZH053rHLVazRW+OGw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/notionists-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.2.4.tgz",
"integrity": "sha512-fskWzBVxQzJhCKqY24DGZbYHSBaauoRa1DgXM7+7xBuksH7mfbTmZTvnUAsAqJYBkla8IPb4ERKduDWtlWYYjQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/open-peeps": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.2.4.tgz",
"integrity": "sha512-s6nwdjXFsplqEI7imlsel4Gt6kFVJm6YIgtZSpry0UdwDoxUUudei5bn957j9lXwVpVUcRjJW+TuEKztYjXkKQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/personas": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.2.4.tgz",
"integrity": "sha512-JNim8RfZYwb0MfxW6DLVfvreCFIevQg+V225Xe5tDfbFgbcYEp4OU/KaiqqO2476OBjCw7i7/8USbv2acBhjwA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/pixel-art": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.2.4.tgz",
"integrity": "sha512-4Ao45asieswUdlCTBZqcoF/0zHR3OWUWB0Mvhlu9b1Fbc6IlPBiOfx2vsp6bnVGVnMag58tJLecx2omeXdECBQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/pixel-art-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.2.4.tgz",
"integrity": "sha512-ZITPLD1cPN4GjKkhWi80s7e5dcbXy34ijWlvmxbc4eb/V7fZSsyRa9EDUW3QStpo+xrCJLcLR+3RBE5iz0PC/A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/rings": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.2.4.tgz",
"integrity": "sha512-teZxELYyV2ogzgb5Mvtn/rHptT0HXo9SjUGS4A52mOwhIdHSGGU71MqA1YUzfae9yJThsw6K7Z9kzuY2LlZZHA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/shapes": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.2.4.tgz",
"integrity": "sha512-MhK9ZdFm1wUnH4zWeKPRMZ98UyApolf5OLzhCywfu38tRN6RVbwtBRHc/42ZwoN1JU1JgXr7hzjYucMqISHtbA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/thumbs": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.2.4.tgz",
"integrity": "sha512-EL4sMqv9p2+1Xy3d8e8UxyeKZV2+cgt3X2x2RTRzEOIIhobtkL8u6lJxmJbiGbpVtVALmrt5e7gjmwqpryYDpg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz",
@ -51484,7 +51932,7 @@
}, },
"packages/client": { "packages/client": {
"name": "@librechat/client", "name": "@librechat/client",
"version": "0.2.6", "version": "0.2.7",
"devDependencies": { "devDependencies": {
"@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.2", "@rollup/plugin-commonjs": "^25.0.2",
@ -51512,6 +51960,8 @@
"peerDependencies": { "peerDependencies": {
"@ariakit/react": "^0.4.16", "@ariakit/react": "^0.4.16",
"@ariakit/react-core": "^0.4.17", "@ariakit/react-core": "^0.4.17",
"@dicebear/collection": "^9.2.2",
"@dicebear/core": "^9.2.2",
"@headlessui/react": "^2.1.2", "@headlessui/react": "^2.1.2",
"@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-alert-dialog": "^1.0.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "@librechat/client", "name": "@librechat/client",
"version": "0.2.6", "version": "0.2.7",
"description": "React components for LibreChat", "description": "React components for LibreChat",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",
@ -64,7 +64,9 @@
"react-hook-form": "^7.56.4", "react-hook-form": "^7.56.4",
"react-resizable-panels": "^3.0.2", "react-resizable-panels": "^3.0.2",
"react-textarea-autosize": "^8.4.0", "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": { "devDependencies": {
"@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-alias": "^5.1.0",

View 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;

View file

@ -33,6 +33,7 @@ export * from './Resizable';
export * from './Select'; export * from './Select';
export { default as Radio } from './Radio'; export { default as Radio } from './Radio';
export { default as Badge } from './Badge'; export { default as Badge } from './Badge';
export { default as Avatar } from './Avatar';
export { default as Combobox } from './Combobox'; export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown'; export { default as Dropdown } from './Dropdown';
export { default as SplitText } from './SplitText'; export { default as SplitText } from './SplitText';

View file

@ -3,6 +3,7 @@
export type { TranslationKeys } from './useLocalize'; export type { TranslationKeys } from './useLocalize';
export { default as useToast } from './useToast'; export { default as useToast } from './useToast';
export { default as useAvatar } from './useAvatar';
export { default as useCombobox } from './useCombobox'; export { default as useCombobox } from './useCombobox';
export { default as useLocalize } from './useLocalize'; export { default as useLocalize } from './useLocalize';
export { default as useMediaQuery } from './useMediaQuery'; export { default as useMediaQuery } from './useMediaQuery';