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>
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
import { useEffect, useCallback } from 'react';
|
|
import { useToastContext } from '@librechat/client';
|
|
import { useFormContext, useWatch } from 'react-hook-form';
|
|
import { mergeFileConfig, fileConfig as defaultFileConfig } from 'librechat-data-provider';
|
|
import type { AgentAvatar } from 'librechat-data-provider';
|
|
import type { AgentForm } from '~/common';
|
|
import { AgentAvatarRender, NoImage, AvatarMenu } from './Images';
|
|
import { useGetFileConfig } from '~/data-provider';
|
|
import { useLocalize } from '~/hooks';
|
|
|
|
function Avatar({ avatar }: { avatar: AgentAvatar | null }) {
|
|
const localize = useLocalize();
|
|
const { showToast } = useToastContext();
|
|
const { control, setValue } = useFormContext<AgentForm>();
|
|
const avatarPreview = useWatch({ control, name: 'avatar_preview' }) ?? '';
|
|
const avatarAction = useWatch({ control, name: 'avatar_action' });
|
|
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
|
select: (data) => mergeFileConfig(data),
|
|
});
|
|
|
|
// Derive whether agent has a remote avatar from the avatar prop
|
|
const hasRemoteAvatar = Boolean(avatar?.filepath);
|
|
|
|
useEffect(() => {
|
|
if (avatarAction) {
|
|
return;
|
|
}
|
|
|
|
if (avatar?.filepath && avatarPreview !== avatar.filepath) {
|
|
setValue('avatar_preview', avatar.filepath);
|
|
}
|
|
|
|
if (!avatar?.filepath && avatarPreview !== '') {
|
|
setValue('avatar_preview', '');
|
|
}
|
|
}, [avatar?.filepath, avatarAction, avatarPreview, setValue]);
|
|
|
|
const handleFileChange = useCallback(
|
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
const sizeLimit = fileConfig.avatarSizeLimit ?? 0;
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
if (sizeLimit && file.size > sizeLimit) {
|
|
const limitInMb = sizeLimit / (1024 * 1024);
|
|
const displayLimit = Number.isInteger(limitInMb)
|
|
? limitInMb
|
|
: parseFloat(limitInMb.toFixed(1));
|
|
showToast({
|
|
message: localize('com_ui_upload_invalid_var', { 0: displayLimit }),
|
|
status: 'error',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setValue('avatar_file', file, { shouldDirty: true });
|
|
setValue('avatar_preview', (reader.result as string) ?? '', { shouldDirty: true });
|
|
setValue('avatar_action', 'upload', { shouldDirty: true });
|
|
};
|
|
reader.readAsDataURL(file);
|
|
},
|
|
[fileConfig.avatarSizeLimit, localize, setValue, showToast],
|
|
);
|
|
|
|
const handleReset = useCallback(() => {
|
|
const remoteAvatarExists = Boolean(avatar?.filepath);
|
|
setValue('avatar_preview', '', { shouldDirty: true });
|
|
setValue('avatar_file', null, { shouldDirty: true });
|
|
setValue('avatar_action', remoteAvatarExists ? 'reset' : null, { shouldDirty: true });
|
|
}, [avatar?.filepath, setValue]);
|
|
|
|
const hasIcon = Boolean(avatarPreview) || hasRemoteAvatar;
|
|
const canReset = hasIcon;
|
|
|
|
return (
|
|
<>
|
|
<div className="flex w-full items-center justify-center gap-4">
|
|
<AvatarMenu
|
|
trigger={
|
|
<button
|
|
type="button"
|
|
className="f h-20 w-20 outline-none ring-offset-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
aria-label={localize('com_ui_upload_agent_avatar_label')}
|
|
>
|
|
{avatarPreview ? <AgentAvatarRender url={avatarPreview} /> : <NoImage />}
|
|
</button>
|
|
}
|
|
handleFileChange={handleFileChange}
|
|
onReset={handleReset}
|
|
canReset={canReset}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default Avatar;
|