| import { useState, useRef, useCallback } from 'react'; | |
| import { Import } from 'lucide-react'; | |
| import { useQueryClient } from '@tanstack/react-query'; | |
| import { QueryKeys, TStartupConfig } from 'librechat-data-provider'; | |
| import { Spinner, useToastContext, Label, Button } from '@librechat/client'; | |
| import { useUploadConversationsMutation } from '~/data-provider'; | |
| import { NotificationSeverity } from '~/common'; | |
| import { useLocalize } from '~/hooks'; | |
| import { cn, logger } from '~/utils'; | |
| function ImportConversations() { | |
| const localize = useLocalize(); | |
| const queryClient = useQueryClient(); | |
| const { showToast } = useToastContext(); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const [isUploading, setIsUploading] = useState(false); | |
| const handleSuccess = useCallback(() => { | |
| showToast({ | |
| message: localize('com_ui_import_conversation_success'), | |
| status: NotificationSeverity.SUCCESS, | |
| }); | |
| setIsUploading(false); | |
| }, [localize, showToast]); | |
| const handleError = useCallback( | |
| (error: unknown) => { | |
| logger.error('Import error:', error); | |
| setIsUploading(false); | |
| const isUnsupportedType = error?.toString().includes('Unsupported import type'); | |
| showToast({ | |
| message: localize( | |
| isUnsupportedType | |
| ? 'com_ui_import_conversation_file_type_error' | |
| : 'com_ui_import_conversation_error', | |
| ), | |
| status: NotificationSeverity.ERROR, | |
| }); | |
| }, | |
| [localize, showToast], | |
| ); | |
| const uploadFile = useUploadConversationsMutation({ | |
| onSuccess: handleSuccess, | |
| onError: handleError, | |
| onMutate: () => setIsUploading(true), | |
| }); | |
| const handleFileUpload = useCallback( | |
| async (file: File) => { | |
| try { | |
| const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]); | |
| const maxFileSize = startupConfig?.conversationImportMaxFileSize; | |
| if (maxFileSize && file.size > maxFileSize) { | |
| const size = (maxFileSize / (1024 * 1024)).toFixed(2); | |
| showToast({ | |
| message: localize('com_error_files_upload_too_large', { 0: size }), | |
| status: NotificationSeverity.ERROR, | |
| }); | |
| setIsUploading(false); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', file, encodeURIComponent(file.name || 'File')); | |
| uploadFile.mutate(formData); | |
| } catch (error) { | |
| logger.error('File processing error:', error); | |
| setIsUploading(false); | |
| showToast({ | |
| message: localize('com_ui_import_conversation_upload_error'), | |
| status: NotificationSeverity.ERROR, | |
| }); | |
| } | |
| }, | |
| [uploadFile, showToast, localize, queryClient], | |
| ); | |
| const handleFileChange = useCallback( | |
| (event: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = event.target.files?.[0]; | |
| if (file) { | |
| setIsUploading(true); | |
| handleFileUpload(file); | |
| } | |
| event.target.value = ''; | |
| }, | |
| [handleFileUpload], | |
| ); | |
| const handleImportClick = useCallback(() => { | |
| fileInputRef.current?.click(); | |
| }, []); | |
| const handleKeyDown = useCallback( | |
| (event: React.KeyboardEvent<HTMLButtonElement>) => { | |
| if (event.key === 'Enter' || event.key === ' ') { | |
| event.preventDefault(); | |
| handleImportClick(); | |
| } | |
| }, | |
| [handleImportClick], | |
| ); | |
| const isImportDisabled = isUploading; | |
| return ( | |
| <div className="flex items-center justify-between"> | |
| <Label id="import-conversation-label">{localize('com_ui_import_conversation_info')}</Label> | |
| <Button | |
| variant="outline" | |
| onClick={handleImportClick} | |
| onKeyDown={handleKeyDown} | |
| disabled={isImportDisabled} | |
| aria-label={localize('com_ui_import')} | |
| aria-labelledby="import-conversation-label" | |
| > | |
| {isUploading ? ( | |
| <Spinner className="mr-1 w-4" /> | |
| ) : ( | |
| <Import className="mr-1 flex h-4 w-4 items-center stroke-1" /> | |
| )} | |
| <span>{localize('com_ui_import')}</span> | |
| </Button> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| className={cn('hidden')} | |
| accept=".json" | |
| onChange={handleFileChange} | |
| aria-hidden="true" | |
| /> | |
| </div> | |
| ); | |
| } | |
| export default ImportConversations; | |