LibreChat/api/server/services/Artifacts/update.js
Danny Avila e509ba5be0
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
🪄 fix: Code Block handling in Artifact Updates (#11417)
* Improved detection of code blocks to support both language identifiers and plain code fences.
* Updated tests to cover various scenarios, including edge cases with different language identifiers and multiline content.
* Ensured proper handling of code blocks with trailing whitespace and complex syntax.
2026-01-20 08:45:43 -05:00

134 lines
4.6 KiB
JavaScript

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);
// Find boundaries between ARTIFACT_START and ARTIFACT_END
const contentStart = artifactContent.indexOf('\n', artifactContent.indexOf(ARTIFACT_START)) + 1;
let contentEnd = artifactContent.lastIndexOf(ARTIFACT_END);
// Special case: if contentEnd is 0, it means the only ::: found is at the start of :::artifact
// This indicates an incomplete artifact (no closing :::)
// We need to check that it's exactly at position 0 (the beginning of artifactContent)
if (contentEnd === 0 && artifactContent.indexOf(ARTIFACT_START) === 0) {
contentEnd = artifactContent.length;
}
if (contentStart === -1 || contentEnd === -1) {
return null;
}
// Check if there are code blocks - handle both ```\n and ```lang\n formats
let codeBlockStart = artifactContent.indexOf('```', contentStart);
const codeBlockEnd = artifactContent.lastIndexOf('\n```', contentEnd);
// If we found opening backticks, find the actual newline (skipping any language identifier)
if (codeBlockStart !== -1) {
const newlineAfterBackticks = artifactContent.indexOf('\n', codeBlockStart);
if (newlineAfterBackticks !== -1 && newlineAfterBackticks < contentEnd) {
codeBlockStart = newlineAfterBackticks;
} else {
codeBlockStart = -1;
}
}
// Determine where to look for the original content
let searchStart, searchEnd;
if (codeBlockStart !== -1) {
// Code block starts - searchStart is right after the newline following ```[lang]
searchStart = codeBlockStart + 1; // after the newline
if (codeBlockEnd !== -1 && codeBlockEnd > codeBlockStart) {
// Code block has proper ending
searchEnd = codeBlockEnd;
} else {
// No closing backticks found or they're before the opening (shouldn't happen)
// This might be an incomplete artifact - search to contentEnd
searchEnd = contentEnd;
}
} else {
// No code blocks at all
searchStart = contentStart;
searchEnd = contentEnd;
}
const innerContent = artifactContent.substring(searchStart, searchEnd);
// Remove trailing newline from original for comparison
const originalTrimmed = original.replace(/\n$/, '');
const relativeIndex = innerContent.indexOf(originalTrimmed);
if (relativeIndex === -1) {
return null;
}
const absoluteIndex = artifact.start + searchStart + relativeIndex;
const endText = originalText.substring(absoluteIndex + originalTrimmed.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,
};