import React, { useState, useRef, useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; import AvatarEditor from 'react-avatar-editor'; import { FileImage, RotateCw, Upload } from 'lucide-react'; import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider'; import type { TUser } from 'librechat-data-provider'; import { Slider, Button, Spinner, OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle, OGDialogTrigger, } from '~/components'; import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider'; import { useToastContext } from '~/Providers'; import { cn, formatBytes } from '~/utils'; import { useLocalize } from '~/hooks'; import store from '~/store'; interface AvatarEditorRef { getImageScaledToCanvas: () => HTMLCanvasElement; getImage: () => HTMLImageElement; } function Avatar() { const setUser = useSetRecoilState(store.user); const [scale, setScale] = useState(1); const [rotation, setRotation] = useState(0); const editorRef = useRef(null); const fileInputRef = useRef(null); const openButtonRef = useRef(null); const [image, setImage] = useState(null); const [isDialogOpen, setDialogOpen] = useState(false); const { data: fileConfig = defaultFileConfig } = useGetFileConfig({ select: (data) => mergeFileConfig(data), }); const localize = useLocalize(); const { showToast } = useToastContext(); const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({ onSuccess: (data) => { showToast({ message: localize('com_ui_upload_success') }); setUser((prev) => ({ ...prev, avatar: data.url } as TUser)); openButtonRef.current?.click(); }, onError: (error) => { console.error('Error:', error); showToast({ message: localize('com_ui_upload_error'), status: 'error' }); }, }); const handleFileChange = (event: React.ChangeEvent): void => { const file = event.target.files?.[0]; handleFile(file); }; const handleFile = (file: File | undefined) => { if (fileConfig.avatarSizeLimit != null && file && file.size <= fileConfig.avatarSizeLimit) { setImage(file); setScale(1); setRotation(0); } else { const megabytes = fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2; showToast({ message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }), status: 'error', }); } }; const handleScaleChange = (value: number[]) => { setScale(value[0]); }; const handleRotate = () => { setRotation((prev) => (prev + 90) % 360); }; const handleUpload = () => { if (editorRef.current) { const canvas = editorRef.current.getImageScaledToCanvas(); canvas.toBlob((blob) => { if (blob) { const formData = new FormData(); formData.append('file', blob, 'avatar.png'); formData.append('manual', 'true'); uploadAvatar(formData); } }, 'image/png'); } }; const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); const file = e.dataTransfer.files[0]; handleFile(file); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); }, []); const openFileDialog = () => { fileInputRef.current?.click(); }; const resetImage = useCallback(() => { setImage(null); setScale(1); setRotation(0); }, []); return ( { setDialogOpen(open); if (!open) { resetImage(); setTimeout(() => { openButtonRef.current?.focus(); }, 0); } }} >
{localize('com_nav_profile_picture')} {localize('com_nav_change_picture')}
{image != null ? localize('com_ui_preview') : localize('com_ui_upload_image')}
{image != null ? ( <>
{localize('com_ui_zoom')}
) : (

{localize('com_ui_drag_drop')}

)}
); } export default Avatar;