| import { useState, useEffect, useRef, useMemo } from 'react'; |
| import * as Popover from '@radix-ui/react-popover'; |
| import { useToastContext } from '@librechat/client'; |
| import { useQueryClient } from '@tanstack/react-query'; |
| import { |
| fileConfig as defaultFileConfig, |
| QueryKeys, |
| defaultOrderQuery, |
| mergeFileConfig, |
| } from 'librechat-data-provider'; |
| import type { |
| Metadata, |
| Assistant, |
| AssistantsEndpoint, |
| AssistantCreateParams, |
| AssistantListResponse, |
| } from 'librechat-data-provider'; |
| import type { UseMutationResult } from '@tanstack/react-query'; |
| import { useUploadAssistantAvatarMutation, useGetFileConfig } from '~/data-provider'; |
| import { AssistantAvatar, NoImage, AvatarMenu } from './Images'; |
| import { useAssistantsMapContext } from '~/Providers'; |
| |
| import { useLocalize } from '~/hooks'; |
| import { formatBytes } from '~/utils'; |
|
|
| function Avatar({ |
| endpoint, |
| version, |
| assistant_id, |
| metadata, |
| createMutation, |
| }: { |
| endpoint: AssistantsEndpoint; |
| version: number | string; |
| assistant_id: string | null; |
| metadata: null | Metadata; |
| createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>; |
| }) { |
| |
| const queryClient = useQueryClient(); |
| const assistantsMap = useAssistantsMapContext(); |
| const [menuOpen, setMenuOpen] = useState(false); |
| const [progress, setProgress] = useState<number>(1); |
| const [input, setInput] = useState<File | null>(null); |
| const [previewUrl, setPreviewUrl] = useState<string | null>(null); |
| const lastSeenCreatedId = useRef<string | null>(null); |
| const { data: fileConfig = defaultFileConfig } = useGetFileConfig({ |
| select: (data) => mergeFileConfig(data), |
| }); |
|
|
| const localize = useLocalize(); |
| const { showToast } = useToastContext(); |
|
|
| const activeModel = useMemo(() => { |
| return assistantsMap?.[endpoint][assistant_id ?? '']?.model ?? ''; |
| }, [assistantsMap, endpoint, assistant_id]); |
|
|
| const { mutate: uploadAvatar } = useUploadAssistantAvatarMutation({ |
| onMutate: () => { |
| setProgress(0.4); |
| }, |
| onSuccess: (data, vars) => { |
| if (vars.postCreation !== true) { |
| showToast({ message: localize('com_ui_upload_success') }); |
| } else if (lastSeenCreatedId.current !== createMutation.data?.id) { |
| lastSeenCreatedId.current = createMutation.data?.id ?? ''; |
| } |
|
|
| setInput(null); |
| setPreviewUrl(data.metadata?.avatar as string | null); |
|
|
| const res = queryClient.getQueryData<AssistantListResponse | undefined>([ |
| QueryKeys.assistants, |
| endpoint, |
| defaultOrderQuery, |
| ]); |
|
|
| if (!res?.data || !res) { |
| return; |
| } |
|
|
| const assistants = res.data.map((assistant) => { |
| if (assistant.id === assistant_id) { |
| return { |
| ...assistant, |
| ...data, |
| }; |
| } |
| return assistant; |
| }); |
|
|
| queryClient.setQueryData<AssistantListResponse>( |
| [QueryKeys.assistants, endpoint, defaultOrderQuery], |
| { |
| ...res, |
| data: assistants, |
| }, |
| ); |
|
|
| setProgress(1); |
| }, |
| onError: (error) => { |
| console.error('Error:', error); |
| setInput(null); |
| setPreviewUrl(null); |
| showToast({ message: localize('com_ui_upload_error'), status: 'error' }); |
| setProgress(1); |
| }, |
| }); |
|
|
| useEffect(() => { |
| if (input) { |
| const reader = new FileReader(); |
| reader.onloadend = () => { |
| setPreviewUrl(reader.result as string); |
| }; |
| reader.readAsDataURL(input); |
| } |
| }, [input]); |
|
|
| useEffect(() => { |
| setPreviewUrl((metadata?.avatar as string | undefined) ?? null); |
| }, [metadata]); |
|
|
| useEffect(() => { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const sharedUploadCondition = !!( |
| createMutation.isSuccess && |
| input && |
| previewUrl && |
| previewUrl.includes('base64') |
| ); |
| if (sharedUploadCondition && lastSeenCreatedId.current === createMutation.data.id) { |
| return; |
| } |
|
|
| if (sharedUploadCondition && createMutation.data.id) { |
| console.log('[AssistantAvatar] Uploading Avatar after Assistant Creation'); |
|
|
| const formData = new FormData(); |
| formData.append('file', input, input.name); |
| formData.append('assistant_id', createMutation.data.id); |
|
|
| uploadAvatar({ |
| assistant_id: createMutation.data.id, |
| model: activeModel, |
| postCreation: true, |
| formData, |
| endpoint, |
| version, |
| }); |
| } |
| }, [ |
| createMutation.data, |
| createMutation.isSuccess, |
| input, |
| previewUrl, |
| uploadAvatar, |
| activeModel, |
| endpoint, |
| version, |
| ]); |
|
|
| const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => { |
| const file = event.target.files?.[0]; |
|
|
| if (fileConfig.avatarSizeLimit && file && file.size <= fileConfig.avatarSizeLimit) { |
| if (!file) { |
| console.error('No file selected'); |
| return; |
| } |
|
|
| setInput(file); |
| setMenuOpen(false); |
|
|
| if (!assistant_id) { |
| |
| console.log('[AssistantAvatar] No assistant_id, will wait until form submission + upload'); |
| return; |
| } |
|
|
| const formData = new FormData(); |
| formData.append('file', file, file.name); |
| formData.append('assistant_id', assistant_id); |
|
|
| uploadAvatar({ |
| assistant_id, |
| model: activeModel, |
| formData, |
| endpoint, |
| version, |
| }); |
| } else { |
| const megabytes = fileConfig.avatarSizeLimit ? formatBytes(fileConfig.avatarSizeLimit) : 2; |
| showToast({ |
| message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }), |
| status: 'error', |
| }); |
| } |
|
|
| setMenuOpen(false); |
| }; |
|
|
| return ( |
| <Popover.Root open={menuOpen} onOpenChange={setMenuOpen}> |
| <div className="flex w-full items-center justify-center gap-4"> |
| <Popover.Trigger asChild> |
| <button |
| type="button" |
| className="h-20 w-20" |
| aria-label={localize('com_ui_upload_avatar_label')} |
| > |
| {previewUrl ? <AssistantAvatar url={previewUrl} progress={progress} /> : <NoImage />} |
| </button> |
| </Popover.Trigger> |
| </div> |
| {<AvatarMenu handleFileChange={handleFileChange} />} |
| </Popover.Root> |
| ); |
| } |
|
|
| export default Avatar; |
|
|