Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion api/server/routes/messages.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,83 @@
const express = require('express');
const { ContentTypes } = require('librechat-data-provider');
const { saveConvo, saveMessage, getMessages, updateMessage, deleteMessages } = require('~/models');
const {
saveConvo,
saveMessage,
getMessage,
getMessages,
updateMessage,
deleteMessages,
} = require('~/models');
const { findAllArtifacts, replaceArtifactContent } = require('~/server/services/Artifacts/update');
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
const { countTokens } = require('~/server/utils');
const { logger } = require('~/config');

const router = express.Router();
router.use(requireJwtAuth);

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 || !updated) {
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' });
}

const targetArtifact = artifacts[index];
let updatedText = null;

if (targetArtifact.source === 'content') {
const part = message.content[targetArtifact.partIndex];
updatedText = replaceArtifactContent(part.text, targetArtifact, original, updated);
if (updatedText) {
part.text = updatedText;
}
} else {
updatedText = replaceArtifactContent(message.text, targetArtifact, original, updated);
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 {
Expand Down
81 changes: 81 additions & 0 deletions api/server/services/Artifacts/update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const ARTIFACT_START = ':::artifact';
const ARTIFACT_END = ':::';

/**
* Find all artifact boundaries in the message
* @param {TMessage} message
* @returns {Array<{start: number, end: number, source: 'content'|'text', partIndex?: number}>}
*/
const findAllArtifacts = (message) => {
const artifacts = [];

// Check content parts first
if (message.content?.length) {
message.content.forEach((part, partIndex) => {
if (part.type === 'text' && typeof part.text === 'string') {
let currentIndex = 0;
let start = part.text.indexOf(ARTIFACT_START, currentIndex);

while (start !== -1) {
const end = part.text.indexOf(ARTIFACT_END, start + ARTIFACT_START.length);
artifacts.push({
start,
end: end !== -1 ? end + ARTIFACT_END.length : part.text.length,
source: 'content',
partIndex,
text: part.text,
});

currentIndex = end !== -1 ? end + ARTIFACT_END.length : part.text.length;
start = part.text.indexOf(ARTIFACT_START, currentIndex);
}
}
});
}

// Check message.text if no content parts
if (!artifacts.length && message.text) {
let currentIndex = 0;
let start = message.text.indexOf(ARTIFACT_START, currentIndex);

while (start !== -1) {
const end = message.text.indexOf(ARTIFACT_END, start + ARTIFACT_START.length);
artifacts.push({
start,
end: end !== -1 ? end + ARTIFACT_END.length : message.text.length,
source: 'text',
text: message.text,
});

currentIndex = end !== -1 ? end + ARTIFACT_END.length : message.text.length;
start = message.text.indexOf(ARTIFACT_START, currentIndex);
}
}

return artifacts;
};

const replaceArtifactContent = (originalText, artifact, original, updated) => {
const artifactContent = artifact.text.substring(artifact.start, artifact.end);
const relativeIndex = artifactContent.indexOf(original);

if (relativeIndex === -1) {
return null;
}

const absoluteIndex = artifact.start + relativeIndex;
const endText = originalText.substring(absoluteIndex + original.length);
const hasTrailingNewline = endText.startsWith('\n');

const updatedText =
originalText.substring(0, absoluteIndex) + updated + (hasTrailingNewline ? '' : '\n') + endText;

return updatedText.replace(/\n+(?=```\n:::)/g, '\n');
};

module.exports = {
ARTIFACT_START,
ARTIFACT_END,
findAllArtifacts,
replaceArtifactContent,
};
Loading
Loading