From 2ad097647c4b11f8bc8b9617c30d2bfe34ec309c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 20 Jul 2024 01:51:59 -0400 Subject: [PATCH] =?UTF-8?q?=E2=8C=9A=20fix:=20Wait=20for=20Initial=20Messa?= =?UTF-8?q?ge=20Save=20&=20Correct=20Latest=20Message=20(#3399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: assistants, unsupported assistant, better logging * chore: remove unnecessary logger in validateAssistant middleware * fix: resolve initial conversation save/promise before saving response * chore: Import and organize dependencies in Speech component * fix: conversation statefulness - Latest Message (at index 0) should not be reset if existing convo - add debugging context for clearAllLatestMessages - Added logging concerning latest Message updates (dev mode only) - update latest message Set logic, also checks for change in conversation Id - consolidated latest message helpers to client/src/utils/messages.ts --- api/app/clients/BaseClient.js | 22 +++++++--- api/app/clients/specs/BaseClient.test.js | 29 ++++++++++++- api/models/Conversation.js | 26 ++++++++--- api/server/controllers/assistants/chatV1.js | 10 ++++- api/server/controllers/assistants/chatV2.js | 10 ++++- api/server/middleware/assistants/validate.js | 3 +- api/server/routes/ask/askChatGPTBrowser.js | 6 +-- api/server/routes/ask/bingAI.js | 6 +-- api/server/routes/convos.js | 2 +- api/server/routes/messages.js | 2 +- .../services/Endpoints/anthropic/addTitle.js | 12 ++++-- .../services/Endpoints/assistants/addTitle.js | 12 ++++-- .../services/Endpoints/google/addTitle.js | 12 ++++-- .../services/Endpoints/openAI/addTitle.js | 12 ++++-- api/server/services/Threads/manage.js | 43 ++++++++++++------- client/src/components/Conversations/Convo.tsx | 8 ++-- .../Nav/SettingsTabs/Speech/Speech.tsx | 17 ++++---- client/src/hooks/Chat/useChatFunctions.ts | 10 +++++ .../hooks/Conversations/useConversation.ts | 6 ++- .../Conversations/useNavigateToConvo.tsx | 21 +++++---- .../src/hooks/Messages/useMessageHelpers.tsx | 31 ++++++++----- .../src/hooks/Messages/useMessageProcess.tsx | 32 ++++++++++---- client/src/hooks/useNewConvo.ts | 2 +- client/src/store/families.ts | 21 ++++++--- client/src/utils/messages.ts | 33 +++++++++++--- 25 files changed, 275 insertions(+), 113 deletions(-) diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 0a0d8bf2d1..1a2c6aadc2 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -540,6 +540,9 @@ class BaseClient { const completionTokens = this.getTokenCount(completion); await this.recordTokenUsage({ promptTokens, completionTokens }); } + if (this.userMessagePromise) { + await this.userMessagePromise; + } this.responsePromise = this.saveMessageToDatabase(responseMessage, saveOptions, user); const messageCache = getLogStores(CacheKeys.MESSAGES); messageCache.set( @@ -620,18 +623,23 @@ class BaseClient { unfinished: false, user, }, - { context: 'api/app/clients/BaseClient.js - saveMessageToDatabase' }, + { context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveMessage' }, ); if (this.skipSaveConvo) { return { message: savedMessage }; } - const conversation = await saveConvo(user, { - conversationId: message.conversationId, - endpoint: this.options.endpoint, - endpointType: this.options.endpointType, - ...endpointOptions, - }); + + const conversation = await saveConvo( + this.options.req, + { + conversationId: message.conversationId, + endpoint: this.options.endpoint, + endpointType: this.options.endpointType, + ...endpointOptions, + }, + { context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveConvo' }, + ); return { message: savedMessage, conversation }; } diff --git a/api/app/clients/specs/BaseClient.test.js b/api/app/clients/specs/BaseClient.test.js index 41138cdb1e..e3cc1515c5 100644 --- a/api/app/clients/specs/BaseClient.test.js +++ b/api/app/clients/specs/BaseClient.test.js @@ -1,7 +1,7 @@ const { Constants } = require('librechat-data-provider'); const { initializeFakeClient } = require('./FakeClient'); -jest.mock('../../../lib/db/connectDb'); +jest.mock('~/lib/db/connectDb'); jest.mock('~/models', () => ({ User: jest.fn(), Key: jest.fn(), @@ -631,5 +631,32 @@ describe('BaseClient', () => { }), ); }); + + test('userMessagePromise is awaited before saving response message', async () => { + // Mock the saveMessageToDatabase method + TestClient.saveMessageToDatabase = jest.fn().mockImplementation(() => { + return new Promise((resolve) => setTimeout(resolve, 100)); // Simulate a delay + }); + + // Send a message + const messagePromise = TestClient.sendMessage('Hello, world!'); + + // Wait a short time to ensure the user message save has started + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Check that saveMessageToDatabase has been called once (for the user message) + expect(TestClient.saveMessageToDatabase).toHaveBeenCalledTimes(1); + + // Wait for the message to be fully processed + await messagePromise; + + // Check that saveMessageToDatabase has been called twice (once for user message, once for response) + expect(TestClient.saveMessageToDatabase).toHaveBeenCalledTimes(2); + + // Check the order of calls + const calls = TestClient.saveMessageToDatabase.mock.calls; + expect(calls[0][0].isCreatedByUser).toBe(true); // First call should be for user message + expect(calls[1][0].isCreatedByUser).toBe(false); // Second call should be for response message + }); }); }); diff --git a/api/models/Conversation.js b/api/models/Conversation.js index d5a67f0e4d..969c142180 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -19,18 +19,32 @@ const getConvo = async (user, conversationId) => { module.exports = { Conversation, - saveConvo: async (user, { conversationId, newConversationId, ...convo }) => { + /** + * Saves a conversation to the database. + * @param {Object} req - The request object. + * @param {string} conversationId - The conversation's ID. + * @param {Object} metadata - Additional metadata to log for operation. + * @returns {Promise} The conversation object. + */ + saveConvo: async (req, { conversationId, newConversationId, ...convo }, metadata) => { try { + if (metadata && metadata?.context) { + logger.info(`[saveConvo] ${metadata.context}`); + } const messages = await getMessages({ conversationId }, '_id'); - const update = { ...convo, messages, user }; + const update = { ...convo, messages, user: req.user.id }; if (newConversationId) { update.conversationId = newConversationId; } - const conversation = await Conversation.findOneAndUpdate({ conversationId, user }, update, { - new: true, - upsert: true, - }); + const conversation = await Conversation.findOneAndUpdate( + { conversationId, user: req.user.id }, + update, + { + new: true, + upsert: true, + }, + ); return conversation.toObject(); } catch (error) { diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js index 3107e78c79..b6d9c0a3ba 100644 --- a/api/server/controllers/assistants/chatV1.js +++ b/api/server/controllers/assistants/chatV1.js @@ -383,6 +383,9 @@ const chatV1 = async (req, res) => { return files; }; + /** @type {Promise|undefined} */ + let userMessagePromise; + const initializeThread = async () => { /** @type {[ undefined | MongoFile[]]}*/ const [processedFiles] = await Promise.all([addVisionPrompt(), getRequestFileIds()]); @@ -439,7 +442,7 @@ const chatV1 = async (req, res) => { previousMessages.push(requestMessage); /* asynchronous */ - saveUserMessage({ ...requestMessage, model }); + userMessagePromise = saveUserMessage(req, { ...requestMessage, model }); conversation = { conversationId, @@ -583,7 +586,10 @@ const chatV1 = async (req, res) => { }); res.end(); - await saveAssistantMessage({ ...responseMessage, model }); + if (userMessagePromise) { + await userMessagePromise; + } + await saveAssistantMessage(req, { ...responseMessage, model }); if (parentMessageId === Constants.NO_PARENT && !_thread_id) { addTitle(req, { diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js index 67e106ca0d..9ad260aa47 100644 --- a/api/server/controllers/assistants/chatV2.js +++ b/api/server/controllers/assistants/chatV2.js @@ -246,6 +246,9 @@ const chatV2 = async (req, res) => { } }; + /** @type {Promise|undefined} */ + let userMessagePromise; + const initializeThread = async () => { await getRequestFileIds(); @@ -288,7 +291,7 @@ const chatV2 = async (req, res) => { previousMessages.push(requestMessage); /* asynchronous */ - saveUserMessage({ ...requestMessage, model }); + userMessagePromise = saveUserMessage(req, { ...requestMessage, model }); conversation = { conversationId, @@ -449,7 +452,10 @@ const chatV2 = async (req, res) => { }); res.end(); - await saveAssistantMessage({ ...responseMessage, model }); + if (userMessagePromise) { + await userMessagePromise; + } + await saveAssistantMessage(req, { ...responseMessage, model }); if (parentMessageId === Constants.NO_PARENT && !_thread_id) { addTitle(req, { diff --git a/api/server/middleware/assistants/validate.js b/api/server/middleware/assistants/validate.js index 613503f6c0..a98e8e227f 100644 --- a/api/server/middleware/assistants/validate.js +++ b/api/server/middleware/assistants/validate.js @@ -19,7 +19,8 @@ const validateAssistant = async (req, res, next) => { } const { supportedIds, excludedIds } = assistantsConfig; - const error = { message: 'Assistant not supported' }; + const error = { message: 'validateAssistant: Assistant not supported' }; + if (supportedIds?.length && !supportedIds.includes(assistant_id)) { return await handleAbortError(res, req, error, { sender: 'System', diff --git a/api/server/routes/ask/askChatGPTBrowser.js b/api/server/routes/ask/askChatGPTBrowser.js index 8b4be397a0..efa4cb8306 100644 --- a/api/server/routes/ask/askChatGPTBrowser.js +++ b/api/server/routes/ask/askChatGPTBrowser.js @@ -52,7 +52,7 @@ router.post('/', setHeaders, async (req, res) => { if (!overrideParentMessageId) { await saveMessage(req, { ...userMessage, user: req.user.id }); - await saveConvo(req.user.id, { + await saveConvo(req, { ...userMessage, ...endpointOption, conversationId, @@ -183,7 +183,7 @@ const ask = async ({ } } - await saveConvo(user, conversationUpdate); + await saveConvo(req, conversationUpdate); conversationId = newConversationId; // STEP3 update the user message @@ -213,7 +213,7 @@ const ask = async ({ if (userParentMessageId == Constants.NO_PARENT) { // const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage }); const title = await response.details.title; - await saveConvo(user, { + await saveConvo(req, { conversationId: conversationId, title, }); diff --git a/api/server/routes/ask/bingAI.js b/api/server/routes/ask/bingAI.js index b5763c3b3d..124521a33e 100644 --- a/api/server/routes/ask/bingAI.js +++ b/api/server/routes/ask/bingAI.js @@ -71,7 +71,7 @@ router.post('/', setHeaders, async (req, res) => { if (!overrideParentMessageId) { await saveMessage(req, { ...userMessage, user: req.user.id }); - await saveConvo(req.user.id, { + await saveConvo(req, { ...userMessage, ...endpointOption, conversationId, @@ -216,7 +216,7 @@ const ask = async ({ conversationUpdate.invocationId = response.invocationId; } - await saveConvo(user, conversationUpdate); + await saveConvo(req, conversationUpdate); userMessage.messageId = newUserMessageId; // If response has parentMessageId, the fake userMessage.messageId should be updated to the real one. @@ -245,7 +245,7 @@ const ask = async ({ response: responseMessage, }); - await saveConvo(user, { + await saveConvo(req, { conversationId: conversationId, title, }); diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index b22d159827..6fadf243b1 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -104,7 +104,7 @@ router.post('/update', async (req, res) => { const update = req.body.arg; try { - const dbResponse = await saveConvo(req.user.id, update); + const dbResponse = await saveConvo(req, update, { context: 'POST /api/convos/update' }); res.status(201).json(dbResponse); } catch (error) { logger.error('Error updating conversation', error); diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index cfc189af63..f510f31f63 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -30,7 +30,7 @@ router.post('/:conversationId', validateMessageReq, async (req, res) => { if (!savedMessage) { return res.status(400).json({ error: 'Message not saved' }); } - await saveConvo(req.user.id, savedMessage); + await saveConvo(req, savedMessage, { context: 'POST /api/messages/:conversationId' }); res.status(201).json(savedMessage); } catch (error) { logger.error('Error saving message:', error); diff --git a/api/server/services/Endpoints/anthropic/addTitle.js b/api/server/services/Endpoints/anthropic/addTitle.js index 30dddd1c3f..290183f1ea 100644 --- a/api/server/services/Endpoints/anthropic/addTitle.js +++ b/api/server/services/Endpoints/anthropic/addTitle.js @@ -23,10 +23,14 @@ const addTitle = async (req, { text, response, client }) => { const title = await client.titleConvo({ text, responseText: response?.text }); await titleCache.set(key, title, 120000); - await saveConvo(req.user.id, { - conversationId: response.conversationId, - title, - }); + await saveConvo( + req, + { + conversationId: response.conversationId, + title, + }, + { context: 'api/server/services/Endpoints/anthropic/addTitle.js' }, + ); }; module.exports = addTitle; diff --git a/api/server/services/Endpoints/assistants/addTitle.js b/api/server/services/Endpoints/assistants/addTitle.js index 7cca98cc7b..605d174130 100644 --- a/api/server/services/Endpoints/assistants/addTitle.js +++ b/api/server/services/Endpoints/assistants/addTitle.js @@ -19,10 +19,14 @@ const addTitle = async (req, { text, responseText, conversationId, client }) => const title = await client.titleConvo({ text, conversationId, responseText }); await titleCache.set(key, title, 120000); - await saveConvo(req.user.id, { - conversationId, - title, - }); + await saveConvo( + req, + { + conversationId, + title, + }, + { context: 'api/server/services/Endpoints/assistants/addTitle.js' }, + ); }; module.exports = addTitle; diff --git a/api/server/services/Endpoints/google/addTitle.js b/api/server/services/Endpoints/google/addTitle.js index 9088b17b9a..c6eabd6036 100644 --- a/api/server/services/Endpoints/google/addTitle.js +++ b/api/server/services/Endpoints/google/addTitle.js @@ -49,10 +49,14 @@ const addTitle = async (req, { text, response, client }) => { const title = await titleClient.titleConvo({ text, responseText: response?.text }); await titleCache.set(key, title, 120000); - await saveConvo(req.user.id, { - conversationId: response.conversationId, - title, - }); + await saveConvo( + req, + { + conversationId: response.conversationId, + title, + }, + { context: 'api/server/services/Endpoints/google/addTitle.js' }, + ); }; module.exports = addTitle; diff --git a/api/server/services/Endpoints/openAI/addTitle.js b/api/server/services/Endpoints/openAI/addTitle.js index 7bd3fc07a2..968c747ff1 100644 --- a/api/server/services/Endpoints/openAI/addTitle.js +++ b/api/server/services/Endpoints/openAI/addTitle.js @@ -23,10 +23,14 @@ const addTitle = async (req, { text, response, client }) => { const title = await client.titleConvo({ text, responseText: response?.text }); await titleCache.set(key, title, 120000); - await saveConvo(req.user.id, { - conversationId: response.conversationId, - title, - }); + await saveConvo( + req, + { + conversationId: response.conversationId, + title, + }, + { context: 'api/server/services/Endpoints/openAI/addTitle.js' }, + ); }; module.exports = addTitle; diff --git a/api/server/services/Threads/manage.js b/api/server/services/Threads/manage.js index 5e2877bed0..9e7eee70b6 100644 --- a/api/server/services/Threads/manage.js +++ b/api/server/services/Threads/manage.js @@ -41,6 +41,7 @@ async function initThread({ openai, body, thread_id: _thread_id }) { /** * Saves a user message to the DB in the Assistants endpoint format. * + * @param {Object} req - The request object. * @param {Object} params - The parameters of the user message * @param {string} params.user - The user's ID. * @param {string} params.text - The user's prompt. @@ -59,7 +60,7 @@ async function initThread({ openai, body, thread_id: _thread_id }) { * @param {string[]} [params.file_ids] - Optional. List of File IDs attached to the userMessage. * @return {Promise} A promise that resolves to the created run object. */ -async function saveUserMessage(params) { +async function saveUserMessage(req, params) { const tokenCount = await countTokens(params.text); // todo: do this on the frontend @@ -110,14 +111,16 @@ async function saveUserMessage(params) { } const message = await recordMessage(userMessage); - await saveConvo(params.user, convo); - + await saveConvo(req, convo, { + context: 'api/server/services/Threads/manage.js #saveUserMessage', + }); return message; } /** * Saves an Assistant message to the DB in the Assistants endpoint format. * + * @param {Object} req - The request object. * @param {Object} params - The parameters of the Assistant message * @param {string} params.user - The user's ID. * @param {string} params.messageId - The message Id. @@ -134,7 +137,7 @@ async function saveUserMessage(params) { * @param {string} [params.promptPrefix] - Optional: from preset for `additional_instructions` field. * @return {Promise} A promise that resolves to the created run object. */ -async function saveAssistantMessage(params) { +async function saveAssistantMessage(req, params) { // const tokenCount = // TODO: need to count each content part const message = await recordMessage({ @@ -154,14 +157,18 @@ async function saveAssistantMessage(params) { // tokenCount, }); - await saveConvo(params.user, { - endpoint: params.endpoint, - conversationId: params.conversationId, - promptPrefix: params.promptPrefix, - instructions: params.instructions, - assistant_id: params.assistant_id, - model: params.model, - }); + await saveConvo( + req, + { + endpoint: params.endpoint, + conversationId: params.conversationId, + promptPrefix: params.promptPrefix, + instructions: params.instructions, + assistant_id: params.assistant_id, + model: params.model, + }, + { context: 'api/server/services/Threads/manage.js #saveAssistantMessage' }, + ); return message; } @@ -338,10 +345,14 @@ async function syncMessages({ await Promise.all(modifyPromises); await Promise.all(recordPromises); - await saveConvo(openai.req.user.id, { - conversationId, - file_ids: attached_file_ids, - }); + await saveConvo( + openai.req, + { + conversationId, + file_ids: attached_file_ids, + }, + { context: 'api/server/services/Threads/manage.js #syncMessages' }, + ); return result; } diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index 4508e47409..3045b1d1e0 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -1,6 +1,7 @@ import { useRecoilValue } from 'recoil'; import { useParams } from 'react-router-dom'; import { useState, useRef, useMemo } from 'react'; +import { Constants } from 'librechat-data-provider'; import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query'; import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react'; import { useUpdateConversationMutation } from '~/data-provider'; @@ -9,14 +10,14 @@ import { useConversations, useNavigateToConvo } from '~/hooks'; import { NotificationSeverity } from '~/common'; import { ArchiveIcon } from '~/components/svg'; import { useToastContext } from '~/Providers'; -import DropDownMenu from './DropDownMenu'; import ArchiveButton from './ArchiveButton'; +import DropDownMenu from './DropDownMenu'; import DeleteButton from './DeleteButton'; import RenameButton from './RenameButton'; import HoverToggle from './HoverToggle'; +import ShareButton from './ShareButton'; import { cn } from '~/utils'; import store from '~/store'; -import ShareButton from './ShareButton'; type KeyEvent = KeyboardEvent; @@ -51,7 +52,8 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa // set document title document.title = title; - navigateWithLastTools(conversation); + /* Note: Latest Message should not be reset if existing convo */ + navigateWithLastTools(conversation, !conversationId || conversationId === Constants.NEW_CONVO); }; const renameHandler = (e: MouseEvent) => { diff --git a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx index 9ae1138e85..6903980c5a 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx @@ -1,12 +1,9 @@ +import { useRecoilState } from 'recoil'; import * as Tabs from '@radix-ui/react-tabs'; +import { Lightbulb, Cog } from 'lucide-react'; import { SettingsTabValues } from 'librechat-data-provider'; import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { useRecoilState } from 'recoil'; -import { Lightbulb, Cog } from 'lucide-react'; -import { useOnClickOutside, useMediaQuery } from '~/hooks'; -import store from '~/store'; -import { cn } from '~/utils'; -import ConversationModeSwitch from './ConversationModeSwitch'; +import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query'; import { CloudBrowserVoicesSwitch, AutomaticPlaybackSwitch, @@ -24,7 +21,10 @@ import { EngineSTTDropdown, DecibelSelector, } from './STT'; -import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query'; +import ConversationModeSwitch from './ConversationModeSwitch'; +import { useOnClickOutside, useMediaQuery } from '~/hooks'; +import { cn, logger } from '~/utils'; +import store from '~/store'; function Speech() { const [confirmClear, setConfirmClear] = useState(false); @@ -131,8 +131,7 @@ function Speech() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); - console.log(sttExternal); - console.log(ttsExternal); + logger.log({ sttExternal, ttsExternal }); const contentRef = useRef(null); useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []); diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 1c69afe760..07c00c80ec 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -107,6 +107,16 @@ export default function useChatFunctions({ const intermediateId = overrideUserMessageId ?? v4(); parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT; + logger.dir('Ask function called with:', { + index, + latestMessage, + conversationId, + intermediateId, + parentMessageId, + currentMessages, + }); + logger.log('====================================='); + if (conversationId == Constants.NEW_CONVO) { parentMessageId = Constants.NO_PARENT; currentMessages = []; diff --git a/client/src/hooks/Conversations/useConversation.ts b/client/src/hooks/Conversations/useConversation.ts index ada584e00d..3dc99f80aa 100644 --- a/client/src/hooks/Conversations/useConversation.ts +++ b/client/src/hooks/Conversations/useConversation.ts @@ -12,7 +12,7 @@ import type { TModelsConfig, TEndpointsConfig, } from 'librechat-data-provider'; -import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils'; +import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils'; import store from '~/store'; const useConversation = () => { @@ -60,6 +60,10 @@ const useConversation = () => { setMessages(messages); setSubmission({} as TSubmission); resetLatestMessage(); + logger.log( + '[useConversation] Switched to conversation and reset Latest Message', + conversation, + ); if (conversation.conversationId === 'new' && !modelsData) { queryClient.invalidateQueries([QueryKeys.messages, 'new']); diff --git a/client/src/hooks/Conversations/useNavigateToConvo.tsx b/client/src/hooks/Conversations/useNavigateToConvo.tsx index 63af9fcf96..c86fd70d71 100644 --- a/client/src/hooks/Conversations/useNavigateToConvo.tsx +++ b/client/src/hooks/Conversations/useNavigateToConvo.tsx @@ -1,7 +1,7 @@ import { useSetRecoilState } from 'recoil'; import { useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; -import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider'; +import { QueryKeys, EModelEndpoint, LocalStorageKeys, Constants } from 'librechat-data-provider'; import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider'; import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils'; import store from '~/store'; @@ -10,7 +10,7 @@ const useNavigateToConvo = (index = 0) => { const navigate = useNavigate(); const queryClient = useQueryClient(); const clearAllConversations = store.useClearConvoState(); - const clearAllLatestMessages = store.useClearLatestMessages(); + const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`); const setSubmission = useSetRecoilState(store.submissionByIndex(index)); const { setConversation } = store.useCreateConversationAtom(index); @@ -50,10 +50,10 @@ const useNavigateToConvo = (index = 0) => { } clearAllConversations(true); setConversation(convo); - navigate(`/c/${convo.conversationId ?? 'new'}`); + navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`); }; - const navigateWithLastTools = (conversation: TConversation) => { + const navigateWithLastTools = (conversation: TConversation, _resetLatestMessage?: boolean) => { // set conversation to the new conversation if (conversation?.endpoint === EModelEndpoint.gptPlugins) { let lastSelectedTools = []; @@ -63,12 +63,15 @@ const useNavigateToConvo = (index = 0) => { } catch (e) { // console.error(e); } - navigateToConvo({ - ...conversation, - tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools, - }); + navigateToConvo( + { + ...conversation, + tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools, + }, + _resetLatestMessage, + ); } else { - navigateToConvo(conversation); + navigateToConvo(conversation, _resetLatestMessage); } }; diff --git a/client/src/hooks/Messages/useMessageHelpers.tsx b/client/src/hooks/Messages/useMessageHelpers.tsx index 3442dd307b..b5ad809ac5 100644 --- a/client/src/hooks/Messages/useMessageHelpers.tsx +++ b/client/src/hooks/Messages/useMessageHelpers.tsx @@ -1,9 +1,9 @@ import { useEffect, useRef, useCallback } from 'react'; -import { isAssistantsEndpoint } from 'librechat-data-provider'; +import { Constants, isAssistantsEndpoint } from 'librechat-data-provider'; import type { TMessageProps } from '~/common'; import { useChatContext, useAssistantsMapContext } from '~/Providers'; -import { getLatestText, getLengthAndFirstFiveChars } from '~/utils'; import useCopyToClipboard from './useCopyToClipboard'; +import { getTextKey, logger } from '~/utils'; export default function useMessageHelpers(props: TMessageProps) { const latestText = useRef(''); @@ -27,7 +27,8 @@ export default function useMessageHelpers(props: TMessageProps) { const isLast = !children?.length; useEffect(() => { - if (conversation?.conversationId === 'new') { + const convoId = conversation?.conversationId; + if (convoId === Constants.NEW_CONVO) { return; } if (!message) { @@ -37,15 +38,25 @@ export default function useMessageHelpers(props: TMessageProps) { return; } - const text = getLatestText(message); - const textKey = `${message?.messageId ?? ''}${getLengthAndFirstFiveChars(text)}`; + const textKey = getTextKey(message, convoId); - if (textKey === latestText.current) { - return; + // Check for text/conversation change + const logInfo = { + textKey, + 'latestText.current': latestText.current, + messageId: message?.messageId, + convoId, + }; + if ( + textKey !== latestText.current || + (latestText.current && convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2]) + ) { + logger.log('[useMessageHelpers] Setting latest message: ', logInfo); + latestText.current = textKey; + setLatestMessage({ ...message }); + } else { + logger.log('No change in latest message', logInfo); } - - latestText.current = textKey; - setLatestMessage({ ...message }); }, [isLast, message, setLatestMessage, conversation?.conversationId]); const enterEdit = useCallback( diff --git a/client/src/hooks/Messages/useMessageProcess.tsx b/client/src/hooks/Messages/useMessageProcess.tsx index b7a3e3ce1d..ca44e2ae42 100644 --- a/client/src/hooks/Messages/useMessageProcess.tsx +++ b/client/src/hooks/Messages/useMessageProcess.tsx @@ -1,8 +1,9 @@ import { useRecoilValue } from 'recoil'; +import { Constants } from 'librechat-data-provider'; import { useEffect, useRef, useCallback, useMemo, useState } from 'react'; import type { TMessage } from 'librechat-data-provider'; import { useChatContext, useAddedChatContext } from '~/Providers'; -import { getLatestText, getLengthAndFirstFiveChars } from '~/utils'; +import { getTextKey, logger } from '~/utils'; import store from '~/store'; export default function useMessageProcess({ message }: { message?: TMessage | null }) { @@ -26,7 +27,8 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu ); useEffect(() => { - if (conversation?.conversationId === 'new') { + const convoId = conversation?.conversationId; + if (convoId === Constants.NEW_CONVO) { return; } if (!message) { @@ -36,15 +38,27 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu return; } - const text = getLatestText(message); - const textKey = `${message?.messageId ?? ''}${getLengthAndFirstFiveChars(text)}`; + const textKey = getTextKey(message, convoId); - if (textKey === latestText.current) { - return; + // Check for text/conversation change + const logInfo = { + textKey, + 'latestText.current': latestText.current, + messageId: message?.messageId, + convoId, + }; + if ( + textKey !== latestText.current || + (convoId && + latestText.current && + convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2]) + ) { + logger.log('[useMessageProcess] Setting latest message: ', logInfo); + latestText.current = textKey; + setLatestMessage({ ...message }); + } else { + logger.log('No change in latest message', logInfo); } - - latestText.current = textKey; - setLatestMessage({ ...message }); }, [hasNoChildren, message, setLatestMessage, conversation?.conversationId]); const handleScroll = useCallback(() => { diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index 5b3aea95ff..1d5dbeea39 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -34,9 +34,9 @@ const useNewConvo = (index = 0) => { const { data: startupConfig } = useGetStartupConfig(); const clearAllConversations = store.useClearConvoState(); const defaultPreset = useRecoilValue(store.defaultPreset); - const clearAllLatestMessages = store.useClearLatestMessages(); const { setConversation } = store.useCreateConversationAtom(index); const [files, setFiles] = useRecoilState(store.filesByIndex(index)); + const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`); const setSubmission = useSetRecoilState(store.submissionByIndex(index)); const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); diff --git a/client/src/store/families.ts b/client/src/store/families.ts index 13e94dabb7..2ba71c9e18 100644 --- a/client/src/store/families.ts +++ b/client/src/store/families.ts @@ -8,7 +8,7 @@ import { useSetRecoilState, useRecoilCallback, } from 'recoil'; -import { LocalStorageKeys } from 'librechat-data-provider'; +import { LocalStorageKeys, Constants } from 'librechat-data-provider'; import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider'; import type { TOptionSettings, ExtendedFile } from '~/common'; import { storeEndpointSettings, logger } from '~/utils'; @@ -27,6 +27,14 @@ const submissionKeysAtom = atom<(string | number)[]>({ const latestMessageFamily = atomFamily({ key: 'latestMessageByIndex', default: null, + effects: [ + ({ onSet, node }) => { + onSet(async (newValue) => { + const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]); + logger.log('Recoil Effect: Setting latestMessage', { key, newValue }); + }); + }, + ] as const, }); const submissionByIndex = atomFamily({ @@ -41,7 +49,7 @@ const latestMessageKeysSelector = selector<(string | number)[]>({ return keys.filter((key) => get(latestMessageFamily(key)) !== null); }, set: ({ set }, newKeys) => { - logger.log('setting latestMessageKeys', newKeys); + logger.log('setting latestMessageKeys', { newKeys }); set(latestMessageKeysAtom, newKeys); }, }); @@ -279,19 +287,22 @@ function useClearSubmissionState() { return clearAllSubmissions; } -function useClearLatestMessages() { +function useClearLatestMessages(context?: string) { const clearAllLatestMessages = useRecoilCallback( ({ reset, set, snapshot }) => async (skipFirst?: boolean) => { const latestMessageKeys = await snapshot.getPromise(latestMessageKeysSelector); - logger.log('latestMessageKeys', latestMessageKeys); + logger.log('[clearAllLatestMessages] latestMessageKeys', latestMessageKeys); + if (context) { + logger.log(`[clearAllLatestMessages] context: ${context}`); + } for (const key of latestMessageKeys) { if (skipFirst && key == 0) { continue; } - logger.log('resetting latest message', key); + logger.log(`[clearAllLatestMessages] resetting latest message; key: ${key}`); reset(latestMessageFamily(key)); } diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts index 1d83db4958..16d35a518c 100644 --- a/client/src/utils/messages.ts +++ b/client/src/utils/messages.ts @@ -1,13 +1,17 @@ -import { ContentTypes } from 'librechat-data-provider'; +import { ContentTypes, Constants } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider'; -export const getLengthAndFirstFiveChars = (str?: string) => { - const length = str ? str.length : 0; - const firstFiveChars = str ? str.substring(0, 5) : ''; - return `${length}${firstFiveChars}`; +export const getLengthAndLastTenChars = (str?: string): string => { + if (!str) { + return '0'; + } + + const length = str.length; + const lastTenChars = str.slice(-10); + return `${length}${lastTenChars}`; }; -export const getLatestText = (message?: TMessage | null) => { +export const getLatestText = (message?: TMessage | null, includeIndex?: boolean) => { if (!message) { return ''; } @@ -18,9 +22,24 @@ export const getLatestText = (message?: TMessage | null) => { for (let i = message.content.length - 1; i >= 0; i--) { const part = message.content[i]; if (part.type === ContentTypes.TEXT && part[ContentTypes.TEXT]?.value?.length > 0) { - return part[ContentTypes.TEXT].value; + const text = part[ContentTypes.TEXT].value; + if (includeIndex) { + return `${text}-${i}`; + } else { + return text; + } } } } return ''; }; + +export const getTextKey = (message?: TMessage | null, convoId?: string | null) => { + if (!message) { + return ''; + } + const text = getLatestText(message, true); + return `${message.messageId ?? ''}${Constants.COMMON_DIVIDER}${getLengthAndLastTenChars(text)}${ + Constants.COMMON_DIVIDER + }${message.conversationId ?? convoId}`; +};