mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
🏄♂️ fix: Handle SSE Stream Edge Case (#8556)
* refactor: Move draft-related utilities to a new `drafts.ts` file * refactor: auto-save draft logic to use new get/set functions * fix: Ensure `getDraft` properly decodes stored draft values * fix: Handle edge case where stream is cancelled before any response, which creates a blank page
This commit is contained in:
parent
f70e0cf849
commit
4c754c1190
4 changed files with 87 additions and 56 deletions
|
|
@ -4,36 +4,11 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||||
import { LocalStorageKeys, Constants } from 'librechat-data-provider';
|
import { LocalStorageKeys, Constants } from 'librechat-data-provider';
|
||||||
import type { TFile } from 'librechat-data-provider';
|
import type { TFile } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile } from '~/common';
|
import type { ExtendedFile } from '~/common';
|
||||||
|
import { clearDraft, getDraft, setDraft } from '~/utils';
|
||||||
import { useChatFormContext } from '~/Providers';
|
import { useChatFormContext } from '~/Providers';
|
||||||
import { useGetFiles } from '~/data-provider';
|
import { useGetFiles } from '~/data-provider';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const clearDraft = debounce((id?: string | null) => {
|
|
||||||
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id ?? ''}`);
|
|
||||||
}, 2500);
|
|
||||||
|
|
||||||
const encodeBase64 = (plainText: string): string => {
|
|
||||||
try {
|
|
||||||
const textBytes = new TextEncoder().encode(plainText);
|
|
||||||
return btoa(String.fromCharCode(...textBytes));
|
|
||||||
} catch (e) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const decodeBase64 = (base64String: string): string => {
|
|
||||||
try {
|
|
||||||
const bytes = atob(base64String);
|
|
||||||
const uint8Array = new Uint8Array(bytes.length);
|
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
|
||||||
uint8Array[i] = bytes.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return new TextDecoder().decode(uint8Array);
|
|
||||||
} catch (e) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAutoSave = ({
|
export const useAutoSave = ({
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
conversationId: _conversationId,
|
conversationId: _conversationId,
|
||||||
|
|
@ -98,8 +73,11 @@ export const useAutoSave = ({
|
||||||
|
|
||||||
const restoreText = useCallback(
|
const restoreText = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const savedDraft = (localStorage.getItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`) ?? '') || '';
|
const savedDraft = getDraft(id);
|
||||||
setValue('text', decodeBase64(savedDraft));
|
if (!savedDraft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue('text', savedDraft);
|
||||||
},
|
},
|
||||||
[setValue],
|
[setValue],
|
||||||
);
|
);
|
||||||
|
|
@ -113,10 +91,7 @@ export const useAutoSave = ({
|
||||||
if (textAreaRef.current.value === '' || textAreaRef.current.value.length === 1) {
|
if (textAreaRef.current.value === '' || textAreaRef.current.value.length === 1) {
|
||||||
clearDraft(id);
|
clearDraft(id);
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(
|
setDraft({ id, value: textAreaRef.current.value });
|
||||||
`${LocalStorageKeys.TEXT_DRAFT}${id}`,
|
|
||||||
encodeBase64(textAreaRef.current.value),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[textAreaRef],
|
[textAreaRef],
|
||||||
|
|
@ -130,16 +105,7 @@ export const useAutoSave = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInput = debounce((value: string) => {
|
const handleInput = debounce((value: string) => setDraft({ id: conversationId, value }), 750);
|
||||||
if (value && value.length > 1) {
|
|
||||||
localStorage.setItem(
|
|
||||||
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
|
|
||||||
encodeBase64(value),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`);
|
|
||||||
}
|
|
||||||
}, 750);
|
|
||||||
|
|
||||||
const eventListener = (e: Event) => {
|
const eventListener = (e: Event) => {
|
||||||
const target = e.target as HTMLTextAreaElement;
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
|
@ -194,10 +160,7 @@ export const useAutoSave = ({
|
||||||
if (pendingDraft) {
|
if (pendingDraft) {
|
||||||
localStorage.setItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`, pendingDraft);
|
localStorage.setItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`, pendingDraft);
|
||||||
} else if (textAreaRef?.current?.value) {
|
} else if (textAreaRef?.current?.value) {
|
||||||
localStorage.setItem(
|
setDraft({ id: conversationId, value: textAreaRef.current.value });
|
||||||
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
|
|
||||||
encodeBase64(textAreaRef.current.value),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const pendingFileDraft = localStorage.getItem(
|
const pendingFileDraft = localStorage.getItem(
|
||||||
`${LocalStorageKeys.FILES_DRAFT}${Constants.PENDING_CONVO}`,
|
`${LocalStorageKeys.FILES_DRAFT}${Constants.PENDING_CONVO}`,
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import {
|
||||||
QueryKeys,
|
QueryKeys,
|
||||||
Constants,
|
Constants,
|
||||||
EndpointURLs,
|
EndpointURLs,
|
||||||
|
ContentTypes,
|
||||||
tPresetSchema,
|
tPresetSchema,
|
||||||
tMessageSchema,
|
tMessageSchema,
|
||||||
tConvoUpdateSchema,
|
tConvoUpdateSchema,
|
||||||
ContentTypes,
|
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { TMessage, TConversation, EventSubmission } from 'librechat-data-provider';
|
import type { TMessage, TConversation, EventSubmission } from 'librechat-data-provider';
|
||||||
|
|
@ -21,6 +21,7 @@ import type { SetterOrUpdater, Resetter } from 'recoil';
|
||||||
import type { ConversationCursorData } from '~/utils';
|
import type { ConversationCursorData } from '~/utils';
|
||||||
import {
|
import {
|
||||||
logger,
|
logger,
|
||||||
|
setDraft,
|
||||||
scrollToEnd,
|
scrollToEnd,
|
||||||
getAllContentText,
|
getAllContentText,
|
||||||
addConvoToAllQueries,
|
addConvoToAllQueries,
|
||||||
|
|
@ -457,6 +458,38 @@ export default function useEventHandlers({
|
||||||
announcePolite({ message: 'end', isStatus: true });
|
announcePolite({ message: 'end', isStatus: true });
|
||||||
announcePolite({ message: getAllContentText(responseMessage) });
|
announcePolite({ message: getAllContentText(responseMessage) });
|
||||||
|
|
||||||
|
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
|
||||||
|
|
||||||
|
const setFinalMessages = (id: string | null, _messages: TMessage[]) => {
|
||||||
|
setMessages(_messages);
|
||||||
|
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, id], _messages);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Handle edge case where stream is cancelled before any response, which creates a blank page */
|
||||||
|
if (
|
||||||
|
!conversation.conversationId &&
|
||||||
|
responseMessage?.content?.[0]?.['text']?.value ===
|
||||||
|
submission.initialResponse?.content?.[0]?.['text']?.value
|
||||||
|
) {
|
||||||
|
const currentConvoId =
|
||||||
|
(submissionConvo.conversationId ?? conversation.conversationId) || Constants.NEW_CONVO;
|
||||||
|
if (isNewConvo && submissionConvo.conversationId) {
|
||||||
|
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNewChat =
|
||||||
|
location.pathname === `/c/${Constants.NEW_CONVO}` &&
|
||||||
|
currentConvoId === Constants.NEW_CONVO;
|
||||||
|
|
||||||
|
setFinalMessages(currentConvoId, isNewChat ? [] : [...messages]);
|
||||||
|
setDraft({ id: currentConvoId, value: requestMessage?.text });
|
||||||
|
setIsSubmitting(false);
|
||||||
|
if (isNewChat) {
|
||||||
|
navigate(`/c/${Constants.NEW_CONVO}`, { replace: true, state: { focusChat: true } });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* Update messages; if assistants endpoint, client doesn't receive responseMessage */
|
/* Update messages; if assistants endpoint, client doesn't receive responseMessage */
|
||||||
let finalMessages: TMessage[] = [];
|
let finalMessages: TMessage[] = [];
|
||||||
if (runMessages) {
|
if (runMessages) {
|
||||||
|
|
@ -467,11 +500,7 @@ export default function useEventHandlers({
|
||||||
finalMessages = [...messages, requestMessage, responseMessage];
|
finalMessages = [...messages, requestMessage, responseMessage];
|
||||||
}
|
}
|
||||||
if (finalMessages.length > 0) {
|
if (finalMessages.length > 0) {
|
||||||
setMessages(finalMessages);
|
setFinalMessages(conversation.conversationId, finalMessages);
|
||||||
queryClient.setQueryData<TMessage[]>(
|
|
||||||
[QueryKeys.messages, conversation.conversationId],
|
|
||||||
finalMessages,
|
|
||||||
);
|
|
||||||
} else if (
|
} else if (
|
||||||
isAssistantsEndpoint(submissionConvo.endpoint) &&
|
isAssistantsEndpoint(submissionConvo.endpoint) &&
|
||||||
(!submissionConvo.conversationId || submissionConvo.conversationId === Constants.NEW_CONVO)
|
(!submissionConvo.conversationId || submissionConvo.conversationId === Constants.NEW_CONVO)
|
||||||
|
|
@ -482,9 +511,8 @@ export default function useEventHandlers({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
|
if (isNewConvo && submissionConvo.conversationId) {
|
||||||
if (isNewConvo) {
|
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId);
|
||||||
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId as string);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Refresh title */
|
/* Refresh title */
|
||||||
|
|
@ -527,7 +555,7 @@ export default function useEventHandlers({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname === '/c/new') {
|
if (location.pathname === `/c/${Constants.NEW_CONVO}`) {
|
||||||
navigate(`/c/${conversation.conversationId}`, { replace: true });
|
navigate(`/c/${conversation.conversationId}`, { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
client/src/utils/drafts.ts
Normal file
39
client/src/utils/drafts.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
export const clearDraft = debounce((id?: string | null) => {
|
||||||
|
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id ?? ''}`);
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
export const encodeBase64 = (plainText: string): string => {
|
||||||
|
try {
|
||||||
|
const textBytes = new TextEncoder().encode(plainText);
|
||||||
|
return btoa(String.fromCharCode(...textBytes));
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decodeBase64 = (base64String: string): string => {
|
||||||
|
try {
|
||||||
|
const bytes = atob(base64String);
|
||||||
|
const uint8Array = new Uint8Array(bytes.length);
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
uint8Array[i] = bytes.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new TextDecoder().decode(uint8Array);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setDraft = ({ id, value }: { id: string; value?: string }) => {
|
||||||
|
if (value && value.length > 1) {
|
||||||
|
localStorage.setItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`, encodeBase64(value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDraft = (id?: string): string | null =>
|
||||||
|
decodeBase64((localStorage.getItem(`${LocalStorageKeys.TEXT_DRAFT}${id ?? ''}`) ?? '') || '');
|
||||||
|
|
@ -6,6 +6,7 @@ export * from './files';
|
||||||
export * from './latex';
|
export * from './latex';
|
||||||
export * from './theme';
|
export * from './theme';
|
||||||
export * from './forms';
|
export * from './forms';
|
||||||
|
export * from './drafts';
|
||||||
export * from './convos';
|
export * from './convos';
|
||||||
export * from './presets';
|
export * from './presets';
|
||||||
export * from './prompts';
|
export * from './prompts';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue