mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
|
|
import { FileImage } from 'lucide-react';
|
||
|
|
import { useSetRecoilState } from 'recoil';
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import type { TUser } from 'librechat-data-provider';
|
||
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
|
||
|
|
import { useUploadAvatarMutation } from '~/data-provider';
|
||
|
|
import { useToastContext } from '~/Providers';
|
||
|
|
import { Spinner } from '~/components/svg';
|
||
|
|
import { useLocalize } from '~/hooks';
|
||
|
|
import { cn } from '~/utils/';
|
||
|
|
import store from '~/store';
|
||
|
|
|
||
|
|
const sizeLimit = 2 * 1024 * 1024; // 2MB
|
||
|
|
|
||
|
|
function Avatar() {
|
||
|
|
const setUser = useSetRecoilState(store.user);
|
||
|
|
const [input, setinput] = useState<File | null>(null);
|
||
|
|
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
||
|
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const localize = useLocalize();
|
||
|
|
const { showToast } = useToastContext();
|
||
|
|
|
||
|
|
const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({
|
||
|
|
onSuccess: (data) => {
|
||
|
|
showToast({ message: localize('com_ui_upload_success') });
|
||
|
|
setDialogOpen(false);
|
||
|
|
|
||
|
|
setUser((prev) => ({ ...prev, avatar: data.url } as TUser));
|
||
|
|
},
|
||
|
|
onError: (error) => {
|
||
|
|
console.error('Error:', error);
|
||
|
|
showToast({ message: localize('com_ui_upload_error'), status: 'error' });
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (input) {
|
||
|
|
const reader = new FileReader();
|
||
|
|
reader.onloadend = () => {
|
||
|
|
setPreviewUrl(reader.result as string);
|
||
|
|
};
|
||
|
|
reader.readAsDataURL(input);
|
||
|
|
} else {
|
||
|
|
setPreviewUrl(null);
|
||
|
|
}
|
||
|
|
}, [input]);
|
||
|
|
|
||
|
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||
|
|
const file = event.target.files?.[0];
|
||
|
|
|
||
|
|
if (file && file.size <= sizeLimit) {
|
||
|
|
setinput(file);
|
||
|
|
setDialogOpen(true);
|
||
|
|
} else {
|
||
|
|
showToast({
|
||
|
|
message: localize('com_ui_upload_invalid'),
|
||
|
|
status: 'error',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleUpload = () => {
|
||
|
|
if (!input) {
|
||
|
|
console.error('No file selected');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const formData = new FormData();
|
||
|
|
formData.append('input', input, input.name);
|
||
|
|
formData.append('manual', 'true');
|
||
|
|
|
||
|
|
uploadAvatar(formData);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<span>{localize('com_nav_profile_picture')}</span>
|
||
|
|
<label
|
||
|
|
htmlFor={'file-upload-avatar'}
|
||
|
|
className="flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal transition-colors hover:bg-slate-200 hover:text-green-700 dark:bg-transparent dark:text-white dark:hover:bg-gray-800 dark:hover:text-green-500"
|
||
|
|
>
|
||
|
|
<FileImage className="mr-1 flex w-[22px] items-center stroke-1" />
|
||
|
|
<span>{localize('com_nav_change_picture')}</span>
|
||
|
|
<input
|
||
|
|
id={'file-upload-avatar'}
|
||
|
|
value=""
|
||
|
|
type="file"
|
||
|
|
className={cn('hidden')}
|
||
|
|
accept=".png, .jpg"
|
||
|
|
onChange={handleFileChange}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Dialog open={isDialogOpen} onOpenChange={() => setDialogOpen(false)}>
|
||
|
|
<DialogContent
|
||
|
|
className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:h-[350px] md:w-[450px] ')}
|
||
|
|
style={{ borderRadius: '12px' }}
|
||
|
|
>
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
|
||
|
|
{localize('com_ui_preview')}
|
||
|
|
</DialogTitle>
|
||
|
|
</DialogHeader>
|
||
|
|
<div className="flex flex-col items-center justify-center">
|
||
|
|
{previewUrl && (
|
||
|
|
<img
|
||
|
|
src={previewUrl}
|
||
|
|
alt="Preview"
|
||
|
|
className="mb-2 rounded-full"
|
||
|
|
style={{
|
||
|
|
maxWidth: '100%',
|
||
|
|
maxHeight: '150px',
|
||
|
|
width: '150px',
|
||
|
|
height: '150px',
|
||
|
|
objectFit: 'cover',
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
<button
|
||
|
|
className={cn(
|
||
|
|
'mt-4 rounded px-4 py-2 text-white hover:bg-green-600 hover:text-gray-200',
|
||
|
|
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
|
||
|
|
)}
|
||
|
|
onClick={handleUpload}
|
||
|
|
disabled={isUploading}
|
||
|
|
>
|
||
|
|
{isUploading ? (
|
||
|
|
<div className="flex h-6">
|
||
|
|
<Spinner className="icon-sm m-auto" />
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
localize('com_ui_upload')
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default Avatar;
|