mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00

* chore: playwright setup update * refactor: update ChatRoute component with accessible loading spinner with live region * chore(Message): typing * ci: first pass, a11y testing * refactor: update lang attribute in index.html to "en-US" * ci: jsx-a11y dev eslint plugin * ci: jsx plugin * fix: Exclude 'vite.config.ts' from TypeScript compilation for testing * fix(a11y): Remove tabIndex from non-interactive element in MessagesView component * fix(a11y): - Visible, non-interactive elements with click handlers must have at least one keyboard listener.eslintjsx-a11y/click-events-have-key-events - Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.eslintjsx-a11y/no-static-element-interactions chore: remove unused bookmarks panel - fix some "Unexpected nullable boolean value in conditional" warnings * fix(NewChat): a11y, nested button issue, add aria-label, remove implicit role * fix(a11y): - partially address #3515 with `main` landmark other: - eslint@typescript-eslint/strict-boolean-expressions * chore(MenuButton): Use button element instead of div for accessibility * chore: Update TitleButton to use button element for accessibility * chore: Update TitleButton to use button element for accessibility * refactor(ChatMenuItem): Improve focus accessibility and code readability * chore(MenuButton): Update aria-label to dynamically include primaryText * fix(a11y): SearchBar - If a form control does not have a properly associated text label, the function or purpose of that form control may not be presented to screen reader users. Visible form labels also provide visible descriptions and larger clickable targets for form controls which placeholders do not. * chore: remove duplicate SearchBar twcss * fix(a11y): - The edit and copy buttons that are visually hidden are exposed to Assistive technology and are announced to screen reader users. * fix(a11y): visible focus outline * fix(a11y): The button to select the LLM Model has the aria-haspopup and aria- expanded attributes which makes its role ambuguous and unclear. It functions like a combobox but doesn't fully support that interaction and also fucntions like a dialog but doesn't completely support that interaction either. * fix(a11y): fix visible focus outline * fix(a11y): Scroll to bottom button missing accessible name #3474 * fix(a11y): The page lacks any heading structure. There should be at least one H1 and other headings to help users understand the orgainzation of the page and the contents. Note: h1 won't be correct here so made it h2 * fix(a11y): LLM controls aria-labels * fix(a11y): There is no visible focus outline to the 'send message' button * fix(a11y): fix visible focus outline for Fork button * refactor(MessageRender): add focus ring to message cards, consolidate complex conditions, add logger for setting latest message, add tabindex for card * fix: focus border color and fix set latest message card condition * fix(a11y): Adequate contrast for MessageAudio buttton * feat: Add GitHub Actions workflow for accessibility linting * chore: Update GitHub Actions workflow for accessibility linting to include client/src/** path * fix(Nav): navmask and accessibility * fix: Update Nav component to handle potential undefined type in SearchContext * fix(a11y): add focus visibility to attach files button #3475 * fix(a11y): discernible text for NewChat button * fix(a11y): accessible landmark names, all page content in landmarks, ensures landmarks are unique #3514 #3515 * fix(Prompts): update isChatRoute prop to be required in List component * fix(a11y): buttons must have discernible text
342 lines
10 KiB
JavaScript
342 lines
10 KiB
JavaScript
const { z } = require('zod');
|
|
const Message = require('./schema/messageSchema');
|
|
const { logger } = require('~/config');
|
|
|
|
const idSchema = z.string().uuid();
|
|
|
|
/**
|
|
* Saves a message in the database.
|
|
*
|
|
* @async
|
|
* @function saveMessage
|
|
* @param {Express.Request} req - The request object containing user information.
|
|
* @param {Object} params - The message data object.
|
|
* @param {string} params.endpoint - The endpoint where the message originated.
|
|
* @param {string} params.iconURL - The URL of the sender's icon.
|
|
* @param {string} params.messageId - The unique identifier for the message.
|
|
* @param {string} params.newMessageId - The new unique identifier for the message (if applicable).
|
|
* @param {string} params.conversationId - The identifier of the conversation.
|
|
* @param {string} [params.parentMessageId] - The identifier of the parent message, if any.
|
|
* @param {string} params.sender - The identifier of the sender.
|
|
* @param {string} params.text - The text content of the message.
|
|
* @param {boolean} params.isCreatedByUser - Indicates if the message was created by the user.
|
|
* @param {string} [params.error] - Any error associated with the message.
|
|
* @param {boolean} [params.unfinished] - Indicates if the message is unfinished.
|
|
* @param {Object[]} [params.files] - An array of files associated with the message.
|
|
* @param {boolean} [params.isEdited] - Indicates if the message was edited.
|
|
* @param {string} [params.finish_reason] - Reason for finishing the message.
|
|
* @param {number} [params.tokenCount] - The number of tokens in the message.
|
|
* @param {string} [params.plugin] - Plugin associated with the message.
|
|
* @param {string[]} [params.plugins] - An array of plugins associated with the message.
|
|
* @param {string} [params.model] - The model used to generate the message.
|
|
* @param {Object} [metadata] - Additional metadata for this operation
|
|
* @param {string} [metadata.context] - The context of the operation
|
|
* @returns {Promise<TMessage>} The updated or newly inserted message document.
|
|
* @throws {Error} If there is an error in saving the message.
|
|
*/
|
|
async function saveMessage(req, params, metadata) {
|
|
try {
|
|
if (!req || !req.user || !req.user.id) {
|
|
throw new Error('User not authenticated');
|
|
}
|
|
|
|
const {
|
|
text,
|
|
error,
|
|
model,
|
|
files,
|
|
plugin,
|
|
sender,
|
|
plugins,
|
|
iconURL,
|
|
endpoint,
|
|
isEdited,
|
|
messageId,
|
|
unfinished,
|
|
tokenCount,
|
|
newMessageId,
|
|
finish_reason,
|
|
conversationId,
|
|
parentMessageId,
|
|
isCreatedByUser,
|
|
} = params;
|
|
|
|
const validConvoId = idSchema.safeParse(conversationId);
|
|
if (!validConvoId.success) {
|
|
logger.warn(`Invalid conversation ID: ${conversationId}`);
|
|
if (metadata && metadata?.context) {
|
|
logger.info(`---\`saveMessage\` context: ${metadata.context}`);
|
|
}
|
|
|
|
logger.info(`---Invalid conversation ID Params:
|
|
|
|
${JSON.stringify(params, null, 2)}
|
|
|
|
`);
|
|
return;
|
|
}
|
|
|
|
const update = {
|
|
user: req.user.id,
|
|
iconURL,
|
|
endpoint,
|
|
messageId: newMessageId || messageId,
|
|
conversationId,
|
|
parentMessageId,
|
|
sender,
|
|
text,
|
|
isCreatedByUser,
|
|
isEdited,
|
|
finish_reason,
|
|
error,
|
|
unfinished,
|
|
tokenCount,
|
|
plugin,
|
|
plugins,
|
|
model,
|
|
};
|
|
|
|
if (files) {
|
|
update.files = files;
|
|
}
|
|
|
|
const message = await Message.findOneAndUpdate({ messageId, user: req.user.id }, update, {
|
|
upsert: true,
|
|
new: true,
|
|
});
|
|
|
|
return message.toObject();
|
|
} catch (err) {
|
|
logger.error('Error saving message:', err);
|
|
if (metadata && metadata?.context) {
|
|
logger.info(`---\`saveMessage\` context: ${metadata.context}`);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves multiple messages in the database in bulk.
|
|
*
|
|
* @async
|
|
* @function bulkSaveMessages
|
|
* @param {Object[]} messages - An array of message objects to save.
|
|
* @returns {Promise<Object>} The result of the bulk write operation.
|
|
* @throws {Error} If there is an error in saving messages in bulk.
|
|
*/
|
|
async function bulkSaveMessages(messages) {
|
|
try {
|
|
const bulkOps = messages.map((message) => ({
|
|
updateOne: {
|
|
filter: { messageId: message.messageId },
|
|
update: message,
|
|
upsert: true,
|
|
},
|
|
}));
|
|
|
|
const result = await Message.bulkWrite(bulkOps);
|
|
return result;
|
|
} catch (err) {
|
|
logger.error('Error saving messages in bulk:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Records a message in the database.
|
|
*
|
|
* @async
|
|
* @function recordMessage
|
|
* @param {Object} params - The message data object.
|
|
* @param {string} params.user - The identifier of the user.
|
|
* @param {string} params.endpoint - The endpoint where the message originated.
|
|
* @param {string} params.messageId - The unique identifier for the message.
|
|
* @param {string} params.conversationId - The identifier of the conversation.
|
|
* @param {string} [params.parentMessageId] - The identifier of the parent message, if any.
|
|
* @param {Partial<TMessage>} rest - Any additional properties from the TMessage typedef not explicitly listed.
|
|
* @returns {Promise<Object>} The updated or newly inserted message document.
|
|
* @throws {Error} If there is an error in saving the message.
|
|
*/
|
|
async function recordMessage({
|
|
user,
|
|
endpoint,
|
|
messageId,
|
|
conversationId,
|
|
parentMessageId,
|
|
...rest
|
|
}) {
|
|
try {
|
|
// No parsing of convoId as may use threadId
|
|
const message = {
|
|
user,
|
|
endpoint,
|
|
messageId,
|
|
conversationId,
|
|
parentMessageId,
|
|
...rest,
|
|
};
|
|
|
|
return await Message.findOneAndUpdate({ user, messageId }, message, {
|
|
upsert: true,
|
|
new: true,
|
|
});
|
|
} catch (err) {
|
|
logger.error('Error recording message:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the text of a message.
|
|
*
|
|
* @async
|
|
* @function updateMessageText
|
|
* @param {Object} params - The update data object.
|
|
* @param {Object} req - The request object.
|
|
* @param {string} params.messageId - The unique identifier for the message.
|
|
* @param {string} params.text - The new text content of the message.
|
|
* @returns {Promise<void>}
|
|
* @throws {Error} If there is an error in updating the message text.
|
|
*/
|
|
async function updateMessageText(req, { messageId, text }) {
|
|
try {
|
|
await Message.updateOne({ messageId, user: req.user.id }, { text });
|
|
} catch (err) {
|
|
logger.error('Error updating message text:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates a message.
|
|
*
|
|
* @async
|
|
* @function updateMessage
|
|
* @param {Object} message - The message object containing update data.
|
|
* @param {Object} req - The request object.
|
|
* @param {string} message.messageId - The unique identifier for the message.
|
|
* @param {string} [message.text] - The new text content of the message.
|
|
* @param {Object[]} [message.files] - The files associated with the message.
|
|
* @param {boolean} [message.isCreatedByUser] - Indicates if the message was created by the user.
|
|
* @param {string} [message.sender] - The identifier of the sender.
|
|
* @param {number} [message.tokenCount] - The number of tokens in the message.
|
|
* @param {Object} [metadata] - The operation metadata
|
|
* @param {string} [metadata.context] - The operation metadata
|
|
* @returns {Promise<TMessage>} The updated message document.
|
|
* @throws {Error} If there is an error in updating the message or if the message is not found.
|
|
*/
|
|
async function updateMessage(req, message, metadata) {
|
|
try {
|
|
const { messageId, ...update } = message;
|
|
update.isEdited = true;
|
|
const updatedMessage = await Message.findOneAndUpdate(
|
|
{ messageId, user: req.user.id },
|
|
update,
|
|
{
|
|
new: true,
|
|
},
|
|
);
|
|
|
|
if (!updatedMessage) {
|
|
throw new Error('Message not found or user not authorized.');
|
|
}
|
|
|
|
return {
|
|
messageId: updatedMessage.messageId,
|
|
conversationId: updatedMessage.conversationId,
|
|
parentMessageId: updatedMessage.parentMessageId,
|
|
sender: updatedMessage.sender,
|
|
text: updatedMessage.text,
|
|
isCreatedByUser: updatedMessage.isCreatedByUser,
|
|
tokenCount: updatedMessage.tokenCount,
|
|
isEdited: true,
|
|
};
|
|
} catch (err) {
|
|
logger.error('Error updating message:', err);
|
|
if (metadata && metadata?.context) {
|
|
logger.info(`---\`updateMessage\` context: ${metadata.context}`);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes messages in a conversation since a specific message.
|
|
*
|
|
* @async
|
|
* @function deleteMessagesSince
|
|
* @param {Object} params - The parameters object.
|
|
* @param {Object} req - The request object.
|
|
* @param {string} params.messageId - The unique identifier for the message.
|
|
* @param {string} params.conversationId - The identifier of the conversation.
|
|
* @returns {Promise<Number>} The number of deleted messages.
|
|
* @throws {Error} If there is an error in deleting messages.
|
|
*/
|
|
async function deleteMessagesSince(req, { messageId, conversationId }) {
|
|
try {
|
|
const message = await Message.findOne({ messageId, user: req.user.id }).lean();
|
|
|
|
if (message) {
|
|
const query = Message.find({ conversationId, user: req.user.id });
|
|
return await query.deleteMany({
|
|
createdAt: { $gt: message.createdAt },
|
|
});
|
|
}
|
|
return undefined;
|
|
} catch (err) {
|
|
logger.error('Error deleting messages:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves messages from the database.
|
|
* @async
|
|
* @function getMessages
|
|
* @param {Record<string, unknown>} filter - The filter criteria.
|
|
* @param {string | undefined} [select] - The fields to select.
|
|
* @returns {Promise<TMessage[]>} The messages that match the filter criteria.
|
|
* @throws {Error} If there is an error in retrieving messages.
|
|
*/
|
|
async function getMessages(filter, select) {
|
|
try {
|
|
if (select) {
|
|
return await Message.find(filter).select(select).sort({ createdAt: 1 }).lean();
|
|
}
|
|
|
|
return await Message.find(filter).sort({ createdAt: 1 }).lean();
|
|
} catch (err) {
|
|
logger.error('Error getting messages:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes messages from the database.
|
|
*
|
|
* @async
|
|
* @function deleteMessages
|
|
* @param {Object} filter - The filter criteria to find messages to delete.
|
|
* @returns {Promise<Object>} The metadata with count of deleted messages.
|
|
* @throws {Error} If there is an error in deleting messages.
|
|
*/
|
|
async function deleteMessages(filter) {
|
|
try {
|
|
return await Message.deleteMany(filter);
|
|
} catch (err) {
|
|
logger.error('Error deleting messages:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
Message,
|
|
saveMessage,
|
|
bulkSaveMessages,
|
|
recordMessage,
|
|
updateMessageText,
|
|
updateMessage,
|
|
deleteMessagesSince,
|
|
getMessages,
|
|
deleteMessages,
|
|
};
|