const express = require('express'); const { logger } = require('@librechat/data-schemas'); const { ContentTypes } = require('librechat-data-provider'); const { unescapeLaTeX, countTokens } = require('@librechat/api'); const { saveConvo, getMessage, saveMessage, getMessages, updateMessage, deleteMessages, } = require('~/models'); const { findAllArtifacts, replaceArtifactContent } = require('~/server/services/Artifacts/update'); const { requireJwtAuth, validateMessageReq } = require('~/server/middleware'); const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc'); const { getConvosQueried } = require('~/models/Conversation'); const { Message } = require('~/db/models'); const router = express.Router(); router.use(requireJwtAuth); router.get('/', async (req, res) => { try { const user = req.user.id ?? ''; const { cursor = null, sortBy = 'createdAt', sortDirection = 'desc', pageSize: pageSizeRaw, conversationId, messageId, search, } = req.query; const pageSize = parseInt(pageSizeRaw, 10) || 25; let response; const sortField = ['endpoint', 'createdAt', 'updatedAt'].includes(sortBy) ? sortBy : 'createdAt'; const sortOrder = sortDirection === 'asc' ? 1 : -1; if (conversationId && messageId) { const message = await Message.findOne({ conversationId, messageId, user: user, }).lean(); response = { messages: message ? [message] : [], nextCursor: null }; } else if (conversationId) { const filter = { conversationId, user: user }; if (cursor) { filter[sortField] = sortOrder === 1 ? { $gt: cursor } : { $lt: cursor }; } const messages = await Message.find(filter) .sort({ [sortField]: sortOrder }) .limit(pageSize + 1) .lean(); const nextCursor = messages.length > pageSize ? messages.pop()[sortField] : null; response = { messages, nextCursor }; } else if (search) { const searchResults = await Message.meiliSearch(search, { filter: `user = "${user}"` }, true); const messages = searchResults.hits || []; const result = await getConvosQueried(req.user.id, messages, cursor); const messageIds = []; const cleanedMessages = []; for (let i = 0; i < messages.length; i++) { let message = messages[i]; if (message.conversationId.includes('--')) { message.conversationId = cleanUpPrimaryKeyValue(message.conversationId); } if (result.convoMap[message.conversationId]) { messageIds.push(message.messageId); cleanedMessages.push(message); } } const dbMessages = await getMessages({ user, messageId: { $in: messageIds }, }); const dbMessageMap = {}; for (const dbMessage of dbMessages) { dbMessageMap[dbMessage.messageId] = dbMessage; } const activeMessages = []; for (const message of cleanedMessages) { const convo = result.convoMap[message.conversationId]; const dbMessage = dbMessageMap[message.messageId]; activeMessages.push({ ...message, title: convo.title, conversationId: message.conversationId, model: convo.model, isCreatedByUser: dbMessage?.isCreatedByUser, endpoint: dbMessage?.endpoint, iconURL: dbMessage?.iconURL, }); } response = { messages: activeMessages, nextCursor: null }; } else { response = { messages: [], nextCursor: null }; } res.status(200).json(response); } catch (error) { logger.error('Error fetching messages:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.post('/artifact/:messageId', async (req, res) => { try { const { messageId } = req.params; const { index, original, updated } = req.body; if (typeof index !== 'number' || index < 0 || original == null || updated == null) { return res.status(400).json({ error: 'Invalid request parameters' }); } const message = await getMessage({ user: req.user.id, messageId }); if (!message) { return res.status(404).json({ error: 'Message not found' }); } const artifacts = findAllArtifacts(message); if (index >= artifacts.length) { return res.status(400).json({ error: 'Artifact index out of bounds' }); } // Unescape LaTeX preprocessing done by the frontend // The frontend escapes $ signs for display, but the database has unescaped versions const unescapedOriginal = unescapeLaTeX(original); const unescapedUpdated = unescapeLaTeX(updated); const targetArtifact = artifacts[index]; let updatedText = null; if (targetArtifact.source === 'content') { const part = message.content[targetArtifact.partIndex]; updatedText = replaceArtifactContent( part.text, targetArtifact, unescapedOriginal, unescapedUpdated, ); if (updatedText) { part.text = updatedText; } } else { updatedText = replaceArtifactContent( message.text, targetArtifact, unescapedOriginal, unescapedUpdated, ); if (updatedText) { message.text = updatedText; } } if (!updatedText) { return res.status(400).json({ error: 'Original content not found in target artifact' }); } const savedMessage = await saveMessage( req, { messageId, conversationId: message.conversationId, text: message.text, content: message.content, user: req.user.id, }, { context: 'POST /api/messages/artifact/:messageId' }, ); res.status(200).json({ conversationId: savedMessage.conversationId, content: savedMessage.content, text: savedMessage.text, }); } catch (error) { logger.error('Error editing artifact:', error); res.status(500).json({ error: 'Internal server error' }); } }); /* Note: It's necessary to add `validateMessageReq` within route definition for correct params */ router.get('/:conversationId', validateMessageReq, async (req, res) => { try { const { conversationId } = req.params; const messages = await getMessages({ conversationId }, '-_id -__v -user'); res.status(200).json(messages); } catch (error) { logger.error('Error fetching messages:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.post('/:conversationId', validateMessageReq, async (req, res) => { try { const message = req.body; const savedMessage = await saveMessage( req, { ...message, user: req.user.id }, { context: 'POST /api/messages/:conversationId' }, ); if (!savedMessage) { return res.status(400).json({ error: 'Message not saved' }); } await saveConvo(req, savedMessage, { context: 'POST /api/messages/:conversationId' }); res.status(201).json(savedMessage); } catch (error) { logger.error('Error saving message:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { const { conversationId, messageId } = req.params; const message = await getMessages({ conversationId, messageId }, '-_id -__v -user'); if (!message) { return res.status(404).json({ error: 'Message not found' }); } res.status(200).json(message); } catch (error) { logger.error('Error fetching message:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { const { conversationId, messageId } = req.params; const { text, index, model } = req.body; if (index === undefined) { const tokenCount = await countTokens(text, model); const result = await updateMessage(req, { messageId, text, tokenCount }); return res.status(200).json(result); } if (typeof index !== 'number' || index < 0) { return res.status(400).json({ error: 'Invalid index' }); } const message = (await getMessages({ conversationId, messageId }, 'content tokenCount'))?.[0]; if (!message) { return res.status(404).json({ error: 'Message not found' }); } const existingContent = message.content; if (!Array.isArray(existingContent) || index >= existingContent.length) { return res.status(400).json({ error: 'Invalid index' }); } const updatedContent = [...existingContent]; if (!updatedContent[index]) { return res.status(400).json({ error: 'Content part not found' }); } const currentPartType = updatedContent[index].type; if (currentPartType !== ContentTypes.TEXT && currentPartType !== ContentTypes.THINK) { return res.status(400).json({ error: 'Cannot update non-text content' }); } const oldText = updatedContent[index][currentPartType]; updatedContent[index] = { type: currentPartType, [currentPartType]: text }; let tokenCount = message.tokenCount; if (tokenCount !== undefined) { const oldTokenCount = await countTokens(oldText, model); const newTokenCount = await countTokens(text, model); tokenCount = Math.max(0, tokenCount - oldTokenCount) + newTokenCount; } const result = await updateMessage(req, { messageId, content: updatedContent, tokenCount }); return res.status(200).json(result); } catch (error) { logger.error('Error updating message:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (req, res) => { try { const { conversationId, messageId } = req.params; const { feedback } = req.body; const updatedMessage = await updateMessage( req, { messageId, feedback: feedback || null, }, { context: 'updateFeedback' }, ); res.json({ messageId, conversationId, feedback: updatedMessage.feedback, }); } catch (error) { logger.error('Error updating message feedback:', error); res.status(500).json({ error: 'Failed to update feedback' }); } }); router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { const { messageId } = req.params; await deleteMessages({ messageId }); res.status(204).send(); } catch (error) { logger.error('Error deleting message:', error); res.status(500).json({ error: 'Internal server error' }); } }); module.exports = router;