| | 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]); |
| | }; |
| |
|