📜 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:
Marco Beretta 2025-04-15 10:04:00 +02:00 committed by GitHub
parent 77a21719fd
commit 650e9b4f6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 3434 additions and 2139 deletions

View file

@ -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,