📜 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

@ -88,11 +88,13 @@ module.exports = {
*/
saveConvo: async (req, { conversationId, newConversationId, ...convo }, metadata) => {
try {
if (metadata && metadata?.context) {
if (metadata?.context) {
logger.debug(`[saveConvo] ${metadata.context}`);
}
const messages = await getMessages({ conversationId }, '_id');
const update = { ...convo, messages, user: req.user.id };
if (newConversationId) {
update.conversationId = newConversationId;
}
@ -148,75 +150,100 @@ module.exports = {
throw new Error('Failed to save conversations in bulk.');
}
},
getConvosByPage: async (user, pageNumber = 1, pageSize = 25, isArchived = false, tags) => {
const query = { user };
getConvosByCursor: async (
user,
{ cursor, limit = 25, isArchived = false, tags, search, order = 'desc' } = {},
) => {
const filters = [{ user }];
if (isArchived) {
query.isArchived = true;
filters.push({ isArchived: true });
} else {
query.$or = [{ isArchived: false }, { isArchived: { $exists: false } }];
}
if (Array.isArray(tags) && tags.length > 0) {
query.tags = { $in: tags };
filters.push({ $or: [{ isArchived: false }, { isArchived: { $exists: false } }] });
}
query.$and = [{ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] }];
if (Array.isArray(tags) && tags.length > 0) {
filters.push({ tags: { $in: tags } });
}
filters.push({ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] });
if (search) {
try {
const meiliResults = await Conversation.meiliSearch(search);
const matchingIds = Array.isArray(meiliResults.hits)
? meiliResults.hits.map((result) => result.conversationId)
: [];
if (!matchingIds.length) {
return { conversations: [], nextCursor: null };
}
filters.push({ conversationId: { $in: matchingIds } });
} catch (error) {
logger.error('[getConvosByCursor] Error during meiliSearch', error);
return { message: 'Error during meiliSearch' };
}
}
if (cursor) {
filters.push({ updatedAt: { $lt: new Date(cursor) } });
}
const query = filters.length === 1 ? filters[0] : { $and: filters };
try {
const totalConvos = (await Conversation.countDocuments(query)) || 1;
const totalPages = Math.ceil(totalConvos / pageSize);
const convos = await Conversation.find(query)
.sort({ updatedAt: -1 })
.skip((pageNumber - 1) * pageSize)
.limit(pageSize)
.select('conversationId endpoint title createdAt updatedAt user')
.sort({ updatedAt: order === 'asc' ? 1 : -1 })
.limit(limit + 1)
.lean();
return { conversations: convos, pages: totalPages, pageNumber, pageSize };
let nextCursor = null;
if (convos.length > limit) {
const lastConvo = convos.pop();
nextCursor = lastConvo.updatedAt.toISOString();
}
return { conversations: convos, nextCursor };
} catch (error) {
logger.error('[getConvosByPage] Error getting conversations', error);
logger.error('[getConvosByCursor] Error getting conversations', error);
return { message: 'Error getting conversations' };
}
},
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 25) => {
getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => {
try {
if (!convoIds || convoIds.length === 0) {
return { conversations: [], pages: 1, pageNumber, pageSize };
if (!convoIds?.length) {
return { conversations: [], nextCursor: null, convoMap: {} };
}
const conversationIds = convoIds.map((convo) => convo.conversationId);
const results = await Conversation.find({
user,
conversationId: { $in: conversationIds },
$or: [{ expiredAt: { $exists: false } }, { expiredAt: null }],
}).lean();
results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
let filtered = results;
if (cursor && cursor !== 'start') {
const cursorDate = new Date(cursor);
filtered = results.filter((convo) => new Date(convo.updatedAt) < cursorDate);
}
const limited = filtered.slice(0, limit + 1);
let nextCursor = null;
if (limited.length > limit) {
const lastConvo = limited.pop();
nextCursor = lastConvo.updatedAt.toISOString();
}
const cache = {};
const convoMap = {};
const promises = [];
convoIds.forEach((convo) =>
promises.push(
Conversation.findOne({
user,
conversationId: convo.conversationId,
$or: [{ expiredAt: { $exists: false } }, { expiredAt: null }],
}).lean(),
),
);
const results = (await Promise.all(promises)).filter(Boolean);
results.forEach((convo, i) => {
const page = Math.floor(i / pageSize) + 1;
if (!cache[page]) {
cache[page] = [];
}
cache[page].push(convo);
limited.forEach((convo) => {
convoMap[convo.conversationId] = convo;
});
const totalPages = Math.ceil(results.length / pageSize);
cache.pages = totalPages;
cache.pageSize = pageSize;
return {
cache,
conversations: cache[pageNumber] || [],
pages: totalPages || 1,
pageNumber,
pageSize,
convoMap,
};
return { conversations: limited, nextCursor, convoMap };
} catch (error) {
logger.error('[getConvosQueried] Error getting conversations', error);
return { message: 'Error fetching conversations' };
@ -257,10 +284,26 @@ module.exports = {
* logger.error(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } }
*/
deleteConvos: async (user, filter) => {
let toRemove = await Conversation.find({ ...filter, user }).select('conversationId');
const ids = toRemove.map((instance) => instance.conversationId);
let deleteCount = await Conversation.deleteMany({ ...filter, user });
deleteCount.messages = await deleteMessages({ conversationId: { $in: ids } });
return deleteCount;
try {
const userFilter = { ...filter, user };
const conversations = await Conversation.find(userFilter).select('conversationId');
const conversationIds = conversations.map((c) => c.conversationId);
if (!conversationIds.length) {
throw new Error('Conversation not found or already deleted.');
}
const deleteConvoResult = await Conversation.deleteMany(userFilter);
const deleteMessagesResult = await deleteMessages({
conversationId: { $in: conversationIds },
});
return { ...deleteConvoResult, messages: deleteMessagesResult };
} catch (error) {
logger.error('[deleteConvos] Error deleting conversations and messages', error);
throw error;
}
},
};