mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-05 15:27:20 +02:00
* ✨ feat: Enhance agent avatar management with upload and reset functionality * ✨ feat: Refactor AvatarMenu to use DropdownPopup for improved UI and functionality * ✨ feat: Improve avatar upload handling in AgentPanel to suppress misleading "no changes" toast * ✨ feat: Refactor toast message handling and payload composition in AgentPanel for improved clarity and functionality * ✨ feat: Enhance agent avatar functionality with upload, reset, and validation improvements * ✨ feat: Refactor agent avatar upload handling and enhance related components for improved functionality and user experience * feat(agents): tighten ACL, harden GETs/search, and sanitize action metadata stop persisting refreshed S3 URLs on GET; compute per-response only enforce ACL EDIT on revert route; remove legacy admin/author/collab checks sanitize action metadata before persisting during duplication (api_key, oauth_client_id, oauth_client_secret) escape user search input, cap length (100), and use Set for public flag mapping add explicit req.file guard in avatar upload; fix empty catch lint; remove unused imports * feat: Remove outdated avatar-related translation keys * feat: Improve error logging for avatar updates and streamline file input handling * feat(agents): implement caching for S3 avatar refresh in agent list responses * fix: replace unconventional 'void e' with explicit comment to clarify intentionally ignored error Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(agents): enhance avatar handling and improve search functionality * fix: clarify intentionally ignored error in agent list handler --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
138 lines
3.5 KiB
TypeScript
138 lines
3.5 KiB
TypeScript
import { useRef, useState, useEffect, type ReactElement } from 'react';
|
|
import * as Ariakit from '@ariakit/react';
|
|
import { DropdownPopup, Skeleton } from '@librechat/client';
|
|
import type { MenuItemProps } from '~/common/menus';
|
|
import { useLocalize } from '~/hooks';
|
|
|
|
export function NoImage() {
|
|
return (
|
|
<div className="border-token-border-medium flex h-full w-full items-center justify-center rounded-full border-2 border-dashed border-black">
|
|
<svg
|
|
stroke="currentColor"
|
|
fill="none"
|
|
strokeWidth="2"
|
|
viewBox="0 0 24 24"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="text-4xl"
|
|
height="1em"
|
|
width="1em"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const AgentAvatarRender = ({ url }: { url?: string }) => {
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
useEffect(() => {
|
|
setIsLoaded(false);
|
|
}, [url]);
|
|
|
|
return (
|
|
<div>
|
|
<div className="relative h-20 w-20 overflow-hidden rounded-full">
|
|
<img
|
|
src={url}
|
|
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
|
|
alt="Agent avatar"
|
|
width="80"
|
|
height="80"
|
|
loading="lazy"
|
|
key={url || 'default-key'}
|
|
onLoad={() => setIsLoaded(true)}
|
|
onError={() => setIsLoaded(false)}
|
|
style={{
|
|
opacity: isLoaded ? 1 : 0,
|
|
transition: 'opacity 0.2s ease-in-out',
|
|
}}
|
|
/>
|
|
{!isLoaded && <Skeleton className="absolute inset-0 rounded-full" aria-hidden="true" />}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export function AvatarMenu({
|
|
trigger,
|
|
handleFileChange,
|
|
onReset,
|
|
canReset,
|
|
}: {
|
|
trigger: ReactElement;
|
|
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
onReset: () => void;
|
|
canReset: boolean;
|
|
}) {
|
|
const localize = useLocalize();
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const onItemClick = () => {
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
}
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
const uploadLabel = localize('com_ui_upload_image');
|
|
|
|
const items: MenuItemProps[] = [
|
|
{
|
|
id: 'upload-avatar',
|
|
label: uploadLabel,
|
|
onClick: () => onItemClick(),
|
|
},
|
|
];
|
|
|
|
if (canReset) {
|
|
items.push(
|
|
{ separate: true },
|
|
{
|
|
id: 'reset-avatar',
|
|
label: localize('com_ui_reset_var', { 0: 'Avatar' }),
|
|
onClick: () => {
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
}
|
|
onReset();
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<DropdownPopup
|
|
trigger={<Ariakit.MenuButton render={trigger} />}
|
|
items={items}
|
|
isOpen={isOpen}
|
|
setIsOpen={setIsOpen}
|
|
menuId="agent-avatar-menu"
|
|
placement="bottom"
|
|
gutter={8}
|
|
portal
|
|
mountByState
|
|
/>
|
|
<input
|
|
accept="image/png,.png,image/jpeg,.jpg,.jpeg,image/gif,.gif,image/webp,.webp"
|
|
multiple={false}
|
|
type="file"
|
|
style={{ display: 'none' }}
|
|
onChange={(event) => {
|
|
handleFileChange(event);
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
} else {
|
|
event.currentTarget.value = '';
|
|
}
|
|
}}
|
|
ref={fileInputRef}
|
|
tabIndex={-1}
|
|
/>
|
|
</>
|
|
);
|
|
}
|