mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-21 17:56:13 +01:00
🪄 fix: Code Block handling in Artifact Updates (#11417)
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
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
* 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.
This commit is contained in:
parent
4a1d2b0d94
commit
e509ba5be0
2 changed files with 277 additions and 4 deletions
|
|
@ -73,15 +73,25 @@ const replaceArtifactContent = (originalText, artifact, original, updated) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Check if there are code blocks
|
||||
const codeBlockStart = artifactContent.indexOf('```\n', contentStart);
|
||||
// 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 = codeBlockStart + 4; // after ```\n
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -494,5 +494,268 @@ ${original}`;
|
|||
/```\n {2}function test\(\) \{\n {4}return \{\n {6}value: 100\n {4}\};\n {2}\}\n```/,
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle code blocks with language identifiers (```svg, ```html, etc.)', () => {
|
||||
const svgContent = `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#4A90A4"/>
|
||||
<rect x="50" y="50" width="100" height="100" fill="#FFFFFF"/>
|
||||
</svg>`;
|
||||
|
||||
/** Artifact with language identifier in code block */
|
||||
const artifactText = `${ARTIFACT_START}{identifier="test-svg" type="image/svg+xml" title="Test SVG"}
|
||||
\`\`\`svg
|
||||
${svgContent}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
expect(artifacts).toHaveLength(1);
|
||||
|
||||
const updatedSvg = svgContent.replace('#FFFFFF', '#131313');
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], svgContent, updatedSvg);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('#131313');
|
||||
expect(result).not.toContain('#FFFFFF');
|
||||
expect(result).toMatch(/```svg\n/);
|
||||
});
|
||||
|
||||
test('should handle code blocks with complex language identifiers', () => {
|
||||
const htmlContent = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test</title></head>
|
||||
<body>Hello</body>
|
||||
</html>`;
|
||||
|
||||
const artifactText = `${ARTIFACT_START}{identifier="test-html" type="text/html" title="Test HTML"}
|
||||
\`\`\`html
|
||||
${htmlContent}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const updatedHtml = htmlContent.replace('Hello', 'Updated');
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], htmlContent, updatedHtml);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('Updated');
|
||||
expect(result).toMatch(/```html\n/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('code block edge cases', () => {
|
||||
test('should handle code block without language identifier (```\\n)', () => {
|
||||
const content = 'const x = 1;\nconst y = 2;';
|
||||
const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('updated');
|
||||
expect(result).toMatch(/```\nupdated\n```/);
|
||||
});
|
||||
|
||||
test('should handle various language identifiers', () => {
|
||||
const languages = [
|
||||
'javascript',
|
||||
'typescript',
|
||||
'python',
|
||||
'jsx',
|
||||
'tsx',
|
||||
'css',
|
||||
'json',
|
||||
'xml',
|
||||
'markdown',
|
||||
'md',
|
||||
];
|
||||
|
||||
for (const lang of languages) {
|
||||
const content = `test content for ${lang}`;
|
||||
const artifactText = `${ARTIFACT_START}{identifier="test-${lang}" type="text/plain" title="Test"}
|
||||
\`\`\`${lang}
|
||||
${content}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
expect(artifacts).toHaveLength(1);
|
||||
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('updated');
|
||||
expect(result).toMatch(new RegExp(`\`\`\`${lang}\\n`));
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle single character language identifier', () => {
|
||||
const content = 'single char lang';
|
||||
const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"}
|
||||
\`\`\`r
|
||||
${content}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('updated');
|
||||
expect(result).toMatch(/```r\n/);
|
||||
});
|
||||
|
||||
test('should handle code block with content that looks like code fence', () => {
|
||||
const content = 'Line 1\nSome text with ``` backticks in middle\nLine 3';
|
||||
const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"}
|
||||
\`\`\`text
|
||||
${content}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('updated');
|
||||
});
|
||||
|
||||
test('should handle code block with trailing whitespace in language line', () => {
|
||||
const content = 'whitespace test';
|
||||
/** Note: trailing spaces after 'python' */
|
||||
const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"}
|
||||
\`\`\`python
|
||||
${content}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('updated');
|
||||
});
|
||||
|
||||
test('should handle react/jsx content with complex syntax', () => {
|
||||
const jsxContent = `function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
return (
|
||||
<div className="app">
|
||||
<h1>Count: {count}</h1>
|
||||
<button onClick={() => setCount(c => c + 1)}>
|
||||
Increment
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}`;
|
||||
|
||||
const artifactText = `${ARTIFACT_START}{identifier="react-app" type="application/vnd.react" title="React App"}
|
||||
\`\`\`jsx
|
||||
${jsxContent}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const updatedJsx = jsxContent.replace('Increment', 'Click me');
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], jsxContent, updatedJsx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('Click me');
|
||||
expect(result).not.toContain('Increment');
|
||||
expect(result).toMatch(/```jsx\n/);
|
||||
});
|
||||
|
||||
test('should handle mermaid diagram content', () => {
|
||||
const mermaidContent = `graph TD
|
||||
A[Start] --> B{Is it?}
|
||||
B -->|Yes| C[OK]
|
||||
B -->|No| D[End]`;
|
||||
|
||||
const artifactText = `${ARTIFACT_START}{identifier="diagram" type="application/vnd.mermaid" title="Flow"}
|
||||
\`\`\`mermaid
|
||||
${mermaidContent}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const updatedMermaid = mermaidContent.replace('Start', 'Begin');
|
||||
const result = replaceArtifactContent(
|
||||
artifactText,
|
||||
artifacts[0],
|
||||
mermaidContent,
|
||||
updatedMermaid,
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('Begin');
|
||||
expect(result).toMatch(/```mermaid\n/);
|
||||
});
|
||||
|
||||
test('should handle artifact without code block (plain text)', () => {
|
||||
const content = 'Just plain text without code fences';
|
||||
const artifactText = `${ARTIFACT_START}{identifier="plain" type="text/plain" title="Plain"}
|
||||
${content}
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const result = replaceArtifactContent(
|
||||
artifactText,
|
||||
artifacts[0],
|
||||
content,
|
||||
'updated plain text',
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('updated plain text');
|
||||
expect(result).not.toContain('```');
|
||||
});
|
||||
|
||||
test('should handle multiline content with various newline patterns', () => {
|
||||
const content = `Line 1
|
||||
Line 2
|
||||
|
||||
Line 4 after empty line
|
||||
Indented line
|
||||
Double indented`;
|
||||
|
||||
const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const updated = content.replace('Line 1', 'First Line');
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], content, updated);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('First Line');
|
||||
expect(result).toContain(' Indented line');
|
||||
expect(result).toContain(' Double indented');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue