| import debounce from 'lodash/debounce'; |
| import { SetterOrUpdater, useRecoilValue } from 'recoil'; |
| import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; |
| import { LocalStorageKeys, Constants } from 'librechat-data-provider'; |
| import type { TFile } from 'librechat-data-provider'; |
| import type { ExtendedFile } from '~/common'; |
| import { clearDraft, getDraft, setDraft } from '~/utils'; |
| import { useChatFormContext } from '~/Providers'; |
| import { useGetFiles } from '~/data-provider'; |
| import store from '~/store'; |
|
|
| export const useAutoSave = ({ |
| isSubmitting, |
| conversationId: _conversationId, |
| textAreaRef, |
| setFiles, |
| files, |
| }: { |
| isSubmitting?: boolean; |
| conversationId?: string | null; |
| textAreaRef?: React.RefObject<HTMLTextAreaElement>; |
| files: Map<string, ExtendedFile>; |
| setFiles: SetterOrUpdater<Map<string, ExtendedFile>>; |
| }) => { |
| |
| const { setValue } = useChatFormContext(); |
| const saveDrafts = useRecoilValue<boolean>(store.saveDrafts); |
| const conversationId = isSubmitting ? Constants.PENDING_CONVO : _conversationId; |
|
|
| const [currentConversationId, setCurrentConversationId] = useState<string | null>(null); |
| const fileIds = useMemo(() => Array.from(files.keys()), [files]); |
| const { data: fileList } = useGetFiles<TFile[]>(); |
|
|
| const restoreFiles = useCallback( |
| (id: string) => { |
| const filesDraft = JSON.parse( |
| (localStorage.getItem(`${LocalStorageKeys.FILES_DRAFT}${id}`) ?? '') || '[]', |
| ) as string[]; |
|
|
| if (filesDraft.length === 0) { |
| setFiles(new Map()); |
| return; |
| } |
|
|
| |
| |
| filesDraft.forEach((fileId) => { |
| const fileData = fileList?.find((f) => f.file_id === fileId); |
| const tempFileData = fileList?.find((f) => f.temp_file_id === fileId); |
| const { fileToRecover, fileIdToRecover } = fileData |
| ? { fileToRecover: fileData, fileIdToRecover: fileId } |
| : { |
| fileToRecover: tempFileData, |
| fileIdToRecover: (tempFileData?.temp_file_id ?? '') || fileId, |
| }; |
|
|
| if (fileToRecover) { |
| setFiles((currentFiles) => { |
| const updatedFiles = new Map(currentFiles); |
| updatedFiles.set(fileIdToRecover, { |
| ...fileToRecover, |
| progress: 1, |
| attached: true, |
| size: fileToRecover.bytes, |
| }); |
| return updatedFiles; |
| }); |
| } |
| }); |
| }, |
| [fileList, setFiles], |
| ); |
|
|
| const restoreText = useCallback( |
| (id: string) => { |
| const savedDraft = getDraft(id); |
| if (!savedDraft) { |
| return; |
| } |
| setValue('text', savedDraft); |
| }, |
| [setValue], |
| ); |
|
|
| const saveText = useCallback( |
| (id: string) => { |
| if (!textAreaRef?.current) { |
| return; |
| } |
| |
| if (textAreaRef.current.value === '' || textAreaRef.current.value.length === 1) { |
| clearDraft(id); |
| } else { |
| setDraft({ id, value: textAreaRef.current.value }); |
| } |
| }, |
| [textAreaRef], |
| ); |
|
|
| useEffect(() => { |
| |
| |
| |
| if (!saveDrafts || conversationId == null || conversationId === '') { |
| return; |
| } |
|
|
| |
| const handleInputFast = debounce( |
| (value: string) => setDraft({ id: conversationId, value }), |
| 65, |
| ); |
|
|
| |
| const handleInputSlow = debounce( |
| (value: string) => setDraft({ id: conversationId, value }), |
| 850, |
| ); |
|
|
| const eventListener = (e: Event) => { |
| const target = e.target as HTMLTextAreaElement; |
| const value = target.value; |
|
|
| |
| handleInputFast.cancel(); |
| handleInputSlow.cancel(); |
|
|
| |
| |
| if (value === '') { |
| handleInputSlow(value); |
| } else { |
| handleInputFast(value); |
| } |
| }; |
|
|
| const textArea = textAreaRef?.current; |
| if (textArea) { |
| textArea.addEventListener('input', eventListener); |
| } |
|
|
| return () => { |
| if (textArea) { |
| textArea.removeEventListener('input', eventListener); |
| } |
| handleInputFast.cancel(); |
| handleInputSlow.cancel(); |
| }; |
| }, [conversationId, saveDrafts, textAreaRef]); |
|
|
| const prevConversationIdRef = useRef<string | null>(null); |
|
|
| useEffect(() => { |
| |
| |
| |
| |
|
|
| if (!saveDrafts || conversationId == null || conversationId === '') { |
| return; |
| } |
| if (conversationId === currentConversationId) { |
| return; |
| } |
|
|
| |
| setFiles(new Map()); |
|
|
| try { |
| |
| if ( |
| prevConversationIdRef.current === Constants.PENDING_CONVO && |
| conversationId !== Constants.PENDING_CONVO && |
| conversationId.length > 3 |
| ) { |
| const pendingDraft = localStorage.getItem( |
| `${LocalStorageKeys.TEXT_DRAFT}${Constants.PENDING_CONVO}`, |
| ); |
|
|
| |
| |
| localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${Constants.PENDING_CONVO}`); |
| if (pendingDraft) { |
| localStorage.setItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`, pendingDraft); |
| } else if (textAreaRef?.current?.value) { |
| setDraft({ id: conversationId, value: textAreaRef.current.value }); |
| } |
| const pendingFileDraft = localStorage.getItem( |
| `${LocalStorageKeys.FILES_DRAFT}${Constants.PENDING_CONVO}`, |
| ); |
|
|
| if (pendingFileDraft) { |
| localStorage.setItem( |
| `${LocalStorageKeys.FILES_DRAFT}${conversationId}`, |
| pendingFileDraft, |
| ); |
| localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${Constants.PENDING_CONVO}`); |
| const filesDraft = JSON.parse(pendingFileDraft || '[]') as string[]; |
| if (filesDraft.length > 0) { |
| restoreFiles(conversationId); |
| } |
| } |
| } else if (currentConversationId != null && currentConversationId) { |
| saveText(currentConversationId); |
| } |
|
|
| restoreText(conversationId); |
| restoreFiles(conversationId); |
| } catch (e) { |
| console.error(e); |
| } |
|
|
| prevConversationIdRef.current = conversationId; |
| setCurrentConversationId(conversationId); |
| }, [ |
| currentConversationId, |
| conversationId, |
| restoreFiles, |
| textAreaRef, |
| restoreText, |
| saveDrafts, |
| saveText, |
| setFiles, |
| ]); |
|
|
| useEffect(() => { |
| |
| |
| |
| |
|
|
| if ( |
| !saveDrafts || |
| conversationId == null || |
| conversationId === '' || |
| currentConversationId !== conversationId |
| ) { |
| return; |
| } |
|
|
| if (fileIds.length === 0) { |
| localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${conversationId}`); |
| } else { |
| localStorage.setItem( |
| `${LocalStorageKeys.FILES_DRAFT}${conversationId}`, |
| JSON.stringify(fileIds), |
| ); |
| } |
| }, [files, conversationId, saveDrafts, currentConversationId, fileIds]); |
| }; |
|
|