mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
📜 refactor: Optimize Conversation History Nav with Cursor Pagination (#5785)
* ✨ feat: improve Nav/Conversations/Convo/NewChat component performance * ✨ feat: implement cursor-based pagination for conversations API * 🔧 refactor: remove createdAt from conversation selection in API and type definitions * 🔧 refactor: include createdAt in conversation selection and update related types * ✨ fix: search functionality and bugs with loadMoreConversations * feat: move ArchivedChats to cursor and DataTable standard * 🔧 refactor: add InfiniteQueryObserverResult type import in Nav component * feat: enhance conversation listing with pagination, sorting, and search capabilities * 🔧 refactor: remove unnecessary comment regarding lodash/debounce in ArchivedChatsTable * 🔧 refactor: remove unused translation keys for archived chats and search results * 🔧 fix: Archived Chats, Delete Convo, Duplicate Convo * 🔧 refactor: improve conversation components with layout adjustments and new translations * 🔧 refactor: simplify archive conversation mutation and improve unarchive handling; fix: update fork mutation * 🔧 refactor: decode search query parameter in conversation route; improve error handling in unarchive mutation; clean up DataTable component styles * 🔧 refactor: remove unused translation key for empty archived chats * 🚀 fix: `archivedConversation` query key not updated correctly while archiving * 🧠 feat: Bedrock Anthropic Reasoning & Update Endpoint Handling (#6163) * feat: Add thinking and thinkingBudget parameters for Bedrock Anthropic models * chore: Update @librechat/agents to version 2.1.8 * refactor: change region order in params * refactor: Add maxTokens parameter to conversation preset schema * refactor: Update agent client to use bedrockInputSchema and improve error handling for model parameters * refactor: streamline/optimize llmConfig initialization and saving for bedrock * fix: ensure config titleModel is used for all endpoints * refactor: enhance OpenAIClient and agent initialization to support endpoint checks for OpenRouter * chore: bump @google/generative-ai * ✨ feat: improve Nav/Conversations/Convo/NewChat component performance * 🔧 refactor: remove unnecessary comment regarding lodash/debounce in ArchivedChatsTable * 🔧 refactor: update translation keys for clarity; simplify conversation query parameters and improve sorting functionality in SharedLinks component * 🔧 refactor: optimize conversation loading logic and improve search handling in Nav component * fix: package-lock * fix: package-lock 2 * fix: package lock 3 * refactor: remove unused utility files and exports to clean up the codebase * refactor: remove i18n and useAuthRedirect modules to streamline codebase * refactor: optimize Conversations component and remove unused ToggleContext * refactor(Convo): add RenameForm and ConvoLink components; enhance Conversations component with responsive design * fix: add missing @azure/storage-blob dependency in package.json * refactor(Search): add error handling with toast notification for search errors * refactor: make createdAt and updatedAt fields of tConvoUpdateSchema less restrictive if timestamps are missing * chore: update @azure/storage-blob dependency to version 12.27.0, ensure package-lock is correct * refactor(Search): improve conversation handling server side * fix: eslint warning and errors * refactor(Search): improved search loading state and overall UX * Refactors conversation cache management Centralizes conversation mutation logic into dedicated utility functions for adding, updating, and removing conversations from query caches. Improves reliability and maintainability by: - Consolidating duplicate cache manipulation code - Adding type safety for infinite query data structures - Implementing consistent cache update patterns across all conversation operations - Removing obsolete conversation helper functions in favor of standardized utilities * fix: conversation handling and SSE event processing - Optimizes conversation state management with useMemo and proper hook ordering - Improves SSE event handler documentation and error handling - Adds reset guard flag for conversation changes - Removes redundant navigation call - Cleans up cursor handling logic and document structure Improves code maintainability and prevents potential race conditions in conversation state updates * refactor: add type for SearchBar `onChange` * fix: type tags * style: rounded to xl all Header buttons * fix: activeConvo in Convo not working * style(Bookmarks): improved UI * a11y(AccountSettings): fixed hover style not visible when using light theme * style(SettingsTabs): improved tab switchers and dropdowns * feat: add translations keys for Speech * chore: fix package-lock * fix(mutations): legacy import after rebase * feat: refactor conversation navigation for accessibility * fix(search): convo and message create/update date not returned * fix(search): show correct iconURL and endpoint for searched messages * fix: small UI improvements * chore: console.log cleanup * chore: fix tests * fix(ChatForm): improve conversation ID handling and clean up useMemo dependencies * chore: improve typing * chore: improve typing * fix(useSSE): clear conversation ID on submission to prevent draft restoration * refactor(OpenAIClient): clean up abort handler * refactor(abortMiddleware): change handleAbort to use function expression * feat: add PENDING_CONVO constant and update conversation ID checks * fix: final event handling on abort * fix: improve title sync and query cache sync on final event * fix: prevent overwriting cached conversation data if it already exists --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
77a21719fd
commit
650e9b4f6c
69 changed files with 3434 additions and 2139 deletions
|
|
@ -15,6 +15,7 @@ import type {
|
|||
TConversation,
|
||||
TEndpointOption,
|
||||
TEndpointsConfig,
|
||||
EndpointSchemaKey,
|
||||
} from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import type { TAskFunction, ExtendedFile } from '~/common';
|
||||
|
|
@ -160,8 +161,8 @@ export default function useChatFunctions({
|
|||
|
||||
// set the endpoint option
|
||||
const convo = parseCompactConvo({
|
||||
endpoint,
|
||||
endpointType,
|
||||
endpoint: endpoint as EndpointSchemaKey,
|
||||
endpointType: endpointType as EndpointSchemaKey,
|
||||
conversation: conversation ?? {},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export { default as usePresets } from './usePresets';
|
|||
export { default as useGetSender } from './useGetSender';
|
||||
export { default as useDefaultConvo } from './useDefaultConvo';
|
||||
export { default as useGenerateConvo } from './useGenerateConvo';
|
||||
export { default as useArchiveHandler } from './useArchiveHandler';
|
||||
export { default as useDebouncedInput } from './useDebouncedInput';
|
||||
export { default as useBookmarkSuccess } from './useBookmarkSuccess';
|
||||
export { default as useNavigateToConvo } from './useNavigateToConvo';
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
|
||||
import { useArchiveConversationMutation } from '~/data-provider';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import useLocalize, { TranslationKeys } from '../useLocalize';
|
||||
import useNewConvo from '../useNewConvo';
|
||||
|
||||
export default function useArchiveHandler(
|
||||
conversationId: string | null,
|
||||
shouldArchive: boolean,
|
||||
retainView: () => void,
|
||||
) {
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
const { showToast } = useToastContext();
|
||||
const { newConversation } = useNewConvo();
|
||||
const { conversationId: currentConvoId } = useParams();
|
||||
|
||||
const archiveConvoMutation = useArchiveConversationMutation(conversationId ?? '');
|
||||
|
||||
return async (e?: MouseEvent | FocusEvent | KeyboardEvent) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
const convoId = conversationId ?? '';
|
||||
if (!convoId) {
|
||||
return;
|
||||
}
|
||||
const label: TranslationKeys = shouldArchive ? 'com_ui_archive_error' : 'com_ui_unarchive_error';
|
||||
archiveConvoMutation.mutate(
|
||||
{ conversationId: convoId, isArchived: shouldArchive },
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (currentConvoId === convoId || currentConvoId === 'new') {
|
||||
newConversation();
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
retainView();
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize(label),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,16 +1,24 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import type { UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||
import type { SearchConversationListResponse } from 'librechat-data-provider';
|
||||
import { useSearchInfiniteQuery, useGetSearchEnabledQuery } from '~/data-provider';
|
||||
import useNewConvo from '~/hooks/useNewConvo';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useSearchMessages({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
export interface UseSearchMessagesResult {
|
||||
searchQuery: string;
|
||||
searchQueryRes: UseInfiniteQueryResult<SearchConversationListResponse, unknown> | undefined;
|
||||
}
|
||||
|
||||
export default function useSearchMessages({
|
||||
isAuthenticated,
|
||||
}: {
|
||||
isAuthenticated: boolean;
|
||||
}): UseSearchMessagesResult {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const { switchToConversation } = useNewConvo();
|
||||
const searchPlaceholderConversation = useCallback(() => {
|
||||
switchToConversation({
|
||||
|
|
@ -25,11 +33,20 @@ export default function useSearchMessages({ isAuthenticated }: { isAuthenticated
|
|||
const searchQuery = useRecoilValue(store.searchQuery);
|
||||
const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled);
|
||||
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearchQuery(searchQuery);
|
||||
}, 350); // 350ms debounce
|
||||
return () => clearTimeout(handler);
|
||||
}, [searchQuery]);
|
||||
|
||||
const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated });
|
||||
const searchQueryRes = useSearchInfiniteQuery(
|
||||
{ pageNumber: pageNumber.toString(), searchQuery: searchQuery, isArchived: false },
|
||||
{ enabled: isAuthenticated && !!searchQuery.length },
|
||||
) as UseInfiniteQueryResult<ConversationListResponse, unknown> | undefined;
|
||||
{ nextCursor: null, search: debouncedSearchQuery, pageSize: 20 },
|
||||
{ enabled: isAuthenticated && !!debouncedSearchQuery },
|
||||
) as UseInfiniteQueryResult<SearchConversationListResponse, unknown> | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery && searchQuery.length > 0) {
|
||||
|
|
@ -64,16 +81,22 @@ export default function useSearchMessages({ isAuthenticated }: { isAuthenticated
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
//we use isInitialLoading here instead of isLoading because query is disabled by default
|
||||
// we use isInitialLoading here instead of isLoading because query is disabled by default
|
||||
if (searchQueryRes?.data) {
|
||||
onSearchSuccess();
|
||||
}
|
||||
}, [searchQueryRes?.data, searchQueryRes?.isInitialLoading, onSearchSuccess]);
|
||||
|
||||
const setIsSearchTyping = useSetRecoilState(store.isSearchTyping);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchQueryRes?.isLoading && !searchQueryRes?.isFetching) {
|
||||
setIsSearchTyping(false);
|
||||
}
|
||||
}, [searchQueryRes?.isLoading, searchQueryRes?.isFetching, setIsSearchTyping]);
|
||||
|
||||
return {
|
||||
pageNumber,
|
||||
searchQuery,
|
||||
setPageNumber,
|
||||
searchQueryRes,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { QueryKeys } from 'librechat-data-provider';
|
|||
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||
import type { InfiniteData } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { updateConvoFields } from '~/utils/convos';
|
||||
import { updateConvoFieldsInfinite } from '~/utils/convos';
|
||||
|
||||
const useUpdateTagsInConvo = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -24,19 +24,25 @@ const useUpdateTagsInConvo = () => {
|
|||
tags,
|
||||
} as t.TConversation;
|
||||
queryClient.setQueryData([QueryKeys.conversation, conversationId], updatedConvo);
|
||||
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return updateConvoFields(
|
||||
convoData,
|
||||
{
|
||||
conversationId: currentConvo.conversationId,
|
||||
tags: updatedConvo.tags,
|
||||
} as t.TConversation,
|
||||
true,
|
||||
);
|
||||
});
|
||||
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
|
||||
[QueryKeys.allConversations],
|
||||
(convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return {
|
||||
...convoData,
|
||||
pages: convoData.pages.map((page) => ({
|
||||
...page,
|
||||
conversations: page.conversations.map((conversation) =>
|
||||
conversation.conversationId === (currentConvo.conversationId ?? '')
|
||||
? { ...conversation, tags: updatedConvo.tags }
|
||||
: conversation,
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// update the tag to newTag in all conversations when a tag is updated to a newTag
|
||||
|
|
@ -54,9 +60,15 @@ const useUpdateTagsInConvo = () => {
|
|||
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
|
||||
const page = newData.pages[pageIndex];
|
||||
page.conversations = page.conversations.map((conversation) => {
|
||||
if (conversation.conversationId && conversation.tags?.includes(tag)) {
|
||||
conversationIdsWithTag.push(conversation.conversationId);
|
||||
conversation.tags = conversation.tags.map((t) => (t === tag ? newTag : t));
|
||||
if (
|
||||
conversation.conversationId &&
|
||||
'tags' in conversation &&
|
||||
Array.isArray((conversation as { tags?: string[] }).tags) &&
|
||||
(conversation as { tags?: string[] }).tags?.includes(tag)
|
||||
) {
|
||||
(conversation as { tags: string[] }).tags = (conversation as { tags: string[] }).tags.map(
|
||||
(t: string) => (t === tag ? newTag : t),
|
||||
);
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ const decodeBase64 = (base64String: string): string => {
|
|||
export const useAutoSave = ({
|
||||
conversationId,
|
||||
textAreaRef,
|
||||
files,
|
||||
setFiles,
|
||||
files,
|
||||
}: {
|
||||
conversationId?: string | null;
|
||||
textAreaRef?: React.RefObject<HTMLTextAreaElement>;
|
||||
|
|
@ -106,7 +106,7 @@ export const useAutoSave = ({
|
|||
return;
|
||||
}
|
||||
// Save the draft of the current conversation before switching
|
||||
if (textAreaRef.current.value === '') {
|
||||
if (textAreaRef.current.value === '' || textAreaRef.current.value.length === 1) {
|
||||
clearDraft(id);
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
|
|
@ -126,8 +126,7 @@ export const useAutoSave = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const handleInput = debounce((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
const handleInput = debounce((value: string) => {
|
||||
if (value && value.length > 1) {
|
||||
localStorage.setItem(
|
||||
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
|
||||
|
|
@ -138,14 +137,19 @@ export const useAutoSave = ({
|
|||
}
|
||||
}, 750);
|
||||
|
||||
const eventListener = (e: Event) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
handleInput(target.value);
|
||||
};
|
||||
|
||||
const textArea = textAreaRef?.current;
|
||||
if (textArea) {
|
||||
textArea.addEventListener('input', handleInput);
|
||||
textArea.addEventListener('input', eventListener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (textArea) {
|
||||
textArea.removeEventListener('input', handleInput);
|
||||
textArea.removeEventListener('input', eventListener);
|
||||
}
|
||||
handleInput.cancel();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,26 +3,33 @@ import React, { useCallback, useEffect, useRef } from 'react';
|
|||
import type { FetchNextPageOptions, InfiniteQueryObserverResult } from '@tanstack/react-query';
|
||||
|
||||
export default function useNavScrolling<TData>({
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
nextCursor,
|
||||
isFetchingNext,
|
||||
setShowLoading,
|
||||
fetchNextPage,
|
||||
}: {
|
||||
hasNextPage?: boolean;
|
||||
isFetchingNextPage: boolean;
|
||||
nextCursor?: string | null;
|
||||
isFetchingNext: boolean;
|
||||
setShowLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
fetchNextPage:
|
||||
| ((
|
||||
options?: FetchNextPageOptions | undefined,
|
||||
) => Promise<InfiniteQueryObserverResult<TData, unknown>>)
|
||||
| undefined;
|
||||
fetchNextPage?: (
|
||||
options?: FetchNextPageOptions | undefined,
|
||||
) => Promise<InfiniteQueryObserverResult<TData, unknown>>;
|
||||
}) {
|
||||
const scrollPositionRef = useRef<number | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const fetchNext = useCallback(
|
||||
throttle(() => (fetchNextPage != null ? fetchNextPage() : () => ({})), 750, { leading: true }),
|
||||
throttle(
|
||||
() => {
|
||||
if (fetchNextPage) {
|
||||
return fetchNextPage();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
750,
|
||||
{ leading: true },
|
||||
),
|
||||
[fetchNextPage],
|
||||
);
|
||||
|
||||
|
|
@ -31,14 +38,14 @@ export default function useNavScrolling<TData>({
|
|||
const { scrollTop, clientHeight, scrollHeight } = containerRef.current;
|
||||
const nearBottomOfList = scrollTop + clientHeight >= scrollHeight * 0.97;
|
||||
|
||||
if (nearBottomOfList && hasNextPage === true && !isFetchingNextPage) {
|
||||
if (nearBottomOfList && nextCursor != null && !isFetchingNext) {
|
||||
setShowLoading(true);
|
||||
fetchNext();
|
||||
} else {
|
||||
setShowLoading(false);
|
||||
}
|
||||
}
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNext, setShowLoading]);
|
||||
}, [nextCursor, isFetchingNext, fetchNext, setShowLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
|
@ -47,16 +54,18 @@ export default function useNavScrolling<TData>({
|
|||
}
|
||||
|
||||
return () => {
|
||||
container?.removeEventListener('scroll', handleScroll);
|
||||
if (container) {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, [handleScroll, fetchNext]);
|
||||
}, [handleScroll]);
|
||||
|
||||
const moveToTop = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
scrollPositionRef.current = container.scrollTop;
|
||||
}
|
||||
}, [containerRef, scrollPositionRef]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
|
|
|
|||
|
|
@ -13,22 +13,19 @@ import {
|
|||
ContentTypes,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
TMessage,
|
||||
TConversation,
|
||||
EventSubmission,
|
||||
ConversationData,
|
||||
} from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater, Resetter } from 'recoil';
|
||||
import type { TMessage, TConversation, EventSubmission } from 'librechat-data-provider';
|
||||
import type { TResData, TFinalResData, ConvoGenerator } from '~/common';
|
||||
import type { InfiniteData } from '@tanstack/react-query';
|
||||
import type { TGenTitleMutation } from '~/data-provider';
|
||||
import type { SetterOrUpdater, Resetter } from 'recoil';
|
||||
import type { ConversationCursorData } from '~/utils';
|
||||
import {
|
||||
scrollToEnd,
|
||||
addConversation,
|
||||
addConvoToAllQueries,
|
||||
updateConvoInAllQueries,
|
||||
removeConvoFromAllQueries,
|
||||
findConversationInInfinite,
|
||||
getAllContentText,
|
||||
deleteConversation,
|
||||
updateConversation,
|
||||
getConversationById,
|
||||
} from '~/utils';
|
||||
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
||||
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
||||
|
|
@ -128,6 +125,37 @@ const createErrorMessage = ({
|
|||
return tMessageSchema.parse(errorMessage);
|
||||
};
|
||||
|
||||
export const getConvoTitle = ({
|
||||
parentId,
|
||||
queryClient,
|
||||
currentTitle,
|
||||
conversationId,
|
||||
}: {
|
||||
parentId?: string | null;
|
||||
queryClient: ReturnType<typeof useQueryClient>;
|
||||
currentTitle?: string | null;
|
||||
conversationId?: string | null;
|
||||
}): string | null | undefined => {
|
||||
if (
|
||||
parentId !== Constants.NO_PARENT &&
|
||||
(currentTitle?.toLowerCase().includes('new chat') ?? false)
|
||||
) {
|
||||
const currentConvo = queryClient.getQueryData<TConversation>([
|
||||
QueryKeys.conversation,
|
||||
conversationId,
|
||||
]);
|
||||
if (currentConvo?.title) {
|
||||
return currentConvo.title;
|
||||
}
|
||||
const convos = queryClient.getQueryData<InfiniteData<ConversationCursorData>>([
|
||||
QueryKeys.allConversations,
|
||||
]);
|
||||
const cachedConvo = findConversationInInfinite(convos, conversationId ?? '');
|
||||
return cachedConvo?.title ?? currentConvo?.title ?? null;
|
||||
}
|
||||
return currentTitle;
|
||||
};
|
||||
|
||||
export default function useEventHandlers({
|
||||
genTitle,
|
||||
setMessages,
|
||||
|
|
@ -186,7 +214,6 @@ export default function useEventHandlers({
|
|||
text,
|
||||
plugin: plugin ?? null,
|
||||
plugins: plugins ?? [],
|
||||
// unfinished: true
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
|
|
@ -198,7 +225,6 @@ export default function useEventHandlers({
|
|||
text,
|
||||
plugin: plugin ?? null,
|
||||
plugins: plugins ?? [],
|
||||
// unfinished: true
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
@ -210,7 +236,6 @@ export default function useEventHandlers({
|
|||
(data: TResData, submission: EventSubmission) => {
|
||||
const { requestMessage, responseMessage, conversation } = data;
|
||||
const { messages, isRegenerate = false } = submission;
|
||||
|
||||
const convoUpdate =
|
||||
(conversation as TConversation | null) ?? (submission.conversation as TConversation);
|
||||
|
||||
|
|
@ -229,12 +254,7 @@ export default function useEventHandlers({
|
|||
|
||||
const isNewConvo = conversation.conversationId !== submission.conversation.conversationId;
|
||||
if (isNewConvo) {
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return deleteConversation(convoData, submission.conversation.conversationId as string);
|
||||
});
|
||||
removeConvoFromAllQueries(queryClient, submission.conversation.conversationId as string);
|
||||
}
|
||||
|
||||
// refresh title
|
||||
|
|
@ -246,11 +266,7 @@ export default function useEventHandlers({
|
|||
|
||||
if (setConversation && !isAddedRequest) {
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...convoUpdate,
|
||||
};
|
||||
|
||||
const update = { ...prevState, ...convoUpdate };
|
||||
return update;
|
||||
});
|
||||
}
|
||||
|
|
@ -264,7 +280,6 @@ export default function useEventHandlers({
|
|||
(data: TSyncData, submission: EventSubmission) => {
|
||||
const { conversationId, thread_id, responseMessage, requestMessage } = data;
|
||||
const { initialResponse, messages: _messages, userMessage } = submission;
|
||||
|
||||
const messages = _messages.filter((msg) => msg.messageId !== userMessage.messageId);
|
||||
|
||||
setMessages([
|
||||
|
|
@ -284,17 +299,13 @@ export default function useEventHandlers({
|
|||
let update = {} as TConversation;
|
||||
if (setConversation && !isAddedRequest) {
|
||||
setConversation((prevState) => {
|
||||
let title = prevState?.title;
|
||||
const parentId = requestMessage.parentMessageId;
|
||||
if (
|
||||
parentId !== Constants.NO_PARENT &&
|
||||
(title?.toLowerCase().includes('new chat') ?? false)
|
||||
) {
|
||||
const convos = queryClient.getQueryData<ConversationData>([QueryKeys.allConversations]);
|
||||
const cachedConvo = getConversationById(convos, conversationId);
|
||||
title = cachedConvo?.title;
|
||||
}
|
||||
|
||||
const title = getConvoTitle({
|
||||
parentId,
|
||||
queryClient,
|
||||
conversationId,
|
||||
currentTitle: prevState?.title,
|
||||
});
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
|
|
@ -302,20 +313,14 @@ export default function useEventHandlers({
|
|||
title,
|
||||
messages: [requestMessage.messageId, responseMessage.messageId],
|
||||
}) as TConversation;
|
||||
|
||||
return update;
|
||||
});
|
||||
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
if (requestMessage.parentMessageId === Constants.NO_PARENT) {
|
||||
return addConversation(convoData, update);
|
||||
} else {
|
||||
return updateConversation(convoData, update);
|
||||
}
|
||||
});
|
||||
if (requestMessage.parentMessageId === Constants.NO_PARENT) {
|
||||
addConvoToAllQueries(queryClient, update);
|
||||
} else {
|
||||
updateConvoInAllQueries(queryClient, update.conversationId!, (_c) => update);
|
||||
}
|
||||
} else if (setConversation) {
|
||||
setConversation((prevState) => {
|
||||
update = tConvoUpdateSchema.parse({
|
||||
|
|
@ -371,39 +376,28 @@ export default function useEventHandlers({
|
|||
}
|
||||
if (setConversation && !isAddedRequest) {
|
||||
setConversation((prevState) => {
|
||||
let title = prevState?.title;
|
||||
const parentId = isRegenerate ? userMessage.overrideParentMessageId : parentMessageId;
|
||||
if (
|
||||
parentId !== Constants.NO_PARENT &&
|
||||
(title?.toLowerCase().includes('new chat') ?? false)
|
||||
) {
|
||||
const convos = queryClient.getQueryData<ConversationData>([QueryKeys.allConversations]);
|
||||
const cachedConvo = getConversationById(convos, conversationId);
|
||||
title = cachedConvo?.title;
|
||||
}
|
||||
|
||||
const title = getConvoTitle({
|
||||
parentId,
|
||||
queryClient,
|
||||
conversationId,
|
||||
currentTitle: prevState?.title,
|
||||
});
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
title,
|
||||
}) as TConversation;
|
||||
|
||||
return update;
|
||||
});
|
||||
|
||||
if (isTemporary) {
|
||||
return;
|
||||
}
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
if (!isTemporary) {
|
||||
if (parentMessageId === Constants.NO_PARENT) {
|
||||
return addConversation(convoData, update);
|
||||
addConvoToAllQueries(queryClient, update);
|
||||
} else {
|
||||
return updateConversation(convoData, update);
|
||||
updateConvoInAllQueries(queryClient, update.conversationId!, (_c) => update);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (setConversation) {
|
||||
setConversation((prevState) => {
|
||||
update = tConvoUpdateSchema.parse({
|
||||
|
|
@ -417,7 +411,6 @@ export default function useEventHandlers({
|
|||
if (resetLatestMessage) {
|
||||
resetLatestMessage();
|
||||
}
|
||||
|
||||
scrollToEnd(() => setAbortScroll(false));
|
||||
},
|
||||
[
|
||||
|
|
@ -447,18 +440,13 @@ export default function useEventHandlers({
|
|||
const currentMessages = getMessages();
|
||||
/* Early return if messages are empty; i.e., the user navigated away */
|
||||
if (!currentMessages || currentMessages.length === 0) {
|
||||
return setIsSubmitting(false);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
/* a11y announcements */
|
||||
announcePolite({
|
||||
message: 'end',
|
||||
isStatus: true,
|
||||
});
|
||||
|
||||
announcePolite({
|
||||
message: getAllContentText(responseMessage),
|
||||
});
|
||||
announcePolite({ message: 'end', isStatus: true });
|
||||
announcePolite({ message: getAllContentText(responseMessage) });
|
||||
|
||||
/* Update messages; if assistants endpoint, client doesn't receive responseMessage */
|
||||
if (runMessages) {
|
||||
|
|
@ -471,12 +459,7 @@ export default function useEventHandlers({
|
|||
|
||||
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
|
||||
if (isNewConvo) {
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return deleteConversation(convoData, submissionConvo.conversationId as string);
|
||||
});
|
||||
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId as string);
|
||||
}
|
||||
|
||||
/* Refresh title */
|
||||
|
|
@ -500,13 +483,18 @@ export default function useEventHandlers({
|
|||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...conversation,
|
||||
...(conversation as TConversation),
|
||||
};
|
||||
|
||||
if (prevState?.model != null && prevState.model !== submissionConvo.model) {
|
||||
update.model = prevState.model;
|
||||
}
|
||||
|
||||
const cachedConvo = queryClient.getQueryData<TConversation>([
|
||||
QueryKeys.conversation,
|
||||
conversation.conversationId,
|
||||
]);
|
||||
if (!cachedConvo) {
|
||||
queryClient.setQueryData([QueryKeys.conversation, conversation.conversationId], update);
|
||||
}
|
||||
return update;
|
||||
});
|
||||
}
|
||||
|
|
@ -530,7 +518,6 @@ export default function useEventHandlers({
|
|||
const errorHandler = useCallback(
|
||||
({ data, submission }: { data?: TResData; submission: EventSubmission }) => {
|
||||
const { messages, userMessage, initialResponse } = submission;
|
||||
|
||||
setCompleted((prev) => new Set(prev.add(initialResponse.messageId)));
|
||||
|
||||
const conversationId =
|
||||
|
|
@ -595,7 +582,6 @@ export default function useEventHandlers({
|
|||
return;
|
||||
}
|
||||
|
||||
console.log('Error:', data);
|
||||
const errorResponse = tMessageSchema.parse({
|
||||
...data,
|
||||
error: true,
|
||||
|
|
@ -619,11 +605,28 @@ export default function useEventHandlers({
|
|||
const abortConversation = useCallback(
|
||||
async (conversationId = '', submission: EventSubmission, messages?: TMessage[]) => {
|
||||
const runAbortKey = `${conversationId}:${messages?.[messages.length - 1]?.messageId ?? ''}`;
|
||||
console.log({ conversationId, submission, messages, runAbortKey });
|
||||
const { endpoint: _endpoint, endpointType } =
|
||||
(submission.conversation as TConversation | null) ?? {};
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
if (!isAssistantsEndpoint(endpoint)) {
|
||||
if (
|
||||
!isAssistantsEndpoint(endpoint) &&
|
||||
messages?.[messages.length - 1] != null &&
|
||||
messages[messages.length - 2] != null
|
||||
) {
|
||||
const requestMessage = messages[messages.length - 2];
|
||||
const responseMessage = messages[messages.length - 1];
|
||||
finalHandler(
|
||||
{
|
||||
conversation: {
|
||||
conversationId,
|
||||
},
|
||||
requestMessage,
|
||||
responseMessage,
|
||||
},
|
||||
submission,
|
||||
);
|
||||
return;
|
||||
} else if (!isAssistantsEndpoint(endpoint)) {
|
||||
if (newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: conversationId || v4() },
|
||||
|
|
@ -651,7 +654,6 @@ export default function useEventHandlers({
|
|||
const contentType = response.headers.get('content-type');
|
||||
if (contentType != null && contentType.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
console.log(`[aborted] RESPONSE STATUS: ${response.status}`, data);
|
||||
if (response.status === 404) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
|
|
@ -662,16 +664,6 @@ export default function useEventHandlers({
|
|||
cancelHandler(data, submission);
|
||||
}
|
||||
} else if (response.status === 204 || response.status === 200) {
|
||||
const responseMessage = {
|
||||
...submission.initialResponse,
|
||||
};
|
||||
|
||||
const data = {
|
||||
requestMessage: submission.userMessage,
|
||||
responseMessage: responseMessage,
|
||||
conversation: submission.conversation,
|
||||
};
|
||||
console.log(`[aborted] RESPONSE STATUS: ${response.status}`, data);
|
||||
setIsSubmitting(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
|
|
@ -682,8 +674,6 @@ export default function useEventHandlers({
|
|||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cancelling request');
|
||||
console.error(error);
|
||||
const errorResponse = createErrorMessage({
|
||||
getMessages,
|
||||
submission,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||
import { v4 } from 'uuid';
|
||||
import { SSE } from 'sse.js';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
request,
|
||||
Constants,
|
||||
|
|
@ -12,12 +13,18 @@ import {
|
|||
removeNullishValues,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { EventSubmission, TMessage, TPayload, TSubmission } from 'librechat-data-provider';
|
||||
import type {
|
||||
EventSubmission,
|
||||
TConversation,
|
||||
TMessage,
|
||||
TPayload,
|
||||
TSubmission,
|
||||
} from 'librechat-data-provider';
|
||||
import type { EventHandlerParams } from './useEventHandlers';
|
||||
import type { TResData } from '~/common';
|
||||
import { useGenTitleMutation, useGetStartupConfig, useGetUserBalance } from '~/data-provider';
|
||||
import useEventHandlers, { getConvoTitle } from './useEventHandlers';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useEventHandlers from './useEventHandlers';
|
||||
import store from '~/store';
|
||||
|
||||
const clearDraft = (conversationId?: string | null) => {
|
||||
|
|
@ -46,6 +53,7 @@ export default function useSSE(
|
|||
isAddedRequest = false,
|
||||
runIndex = 0,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const genTitle = useGenTitleMutation();
|
||||
const setActiveRunId = useSetRecoilState(store.activeRunFamily(runIndex));
|
||||
|
||||
|
|
@ -99,6 +107,30 @@ export default function useSSE(
|
|||
let { userMessage } = submission;
|
||||
|
||||
const payloadData = createPayload(submission);
|
||||
/**
|
||||
* Helps clear text immediately on submission instead of
|
||||
* restoring draft, which gets deleted on generation end
|
||||
* */
|
||||
const parentId = submission?.isRegenerate
|
||||
? userMessage.overrideParentMessageId
|
||||
: userMessage.parentMessageId;
|
||||
setConversation?.((prev: TConversation | null) => {
|
||||
if (!prev) {
|
||||
return null;
|
||||
}
|
||||
const title =
|
||||
getConvoTitle({
|
||||
parentId,
|
||||
queryClient,
|
||||
currentTitle: prev?.title,
|
||||
conversationId: prev?.conversationId,
|
||||
}) ?? '';
|
||||
return {
|
||||
...prev,
|
||||
title,
|
||||
conversationId: Constants.PENDING_CONVO as string,
|
||||
};
|
||||
});
|
||||
let { payload } = payloadData;
|
||||
if (isAssistantsEndpoint(payload.endpoint) || isAgentsEndpoint(payload.endpoint)) {
|
||||
payload = removeNullishValues(payload) as TPayload;
|
||||
|
|
@ -250,5 +282,6 @@ export default function useSSE(
|
|||
sse.dispatchEvent(e);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [submission]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue