diff --git a/app/components/chat/Markdown.spec.ts b/app/components/chat/Markdown.spec.ts new file mode 100644 index 00000000..23817789 --- /dev/null +++ b/app/components/chat/Markdown.spec.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; +import { stripCodeFenceFromArtifact } from './Markdown'; + +describe('stripCodeFenceFromArtifact', () => { + it('should remove code fences around artifact element', () => { + const input = "```xml\n
\n```"; + const expected = "\n\n"; + expect(stripCodeFenceFromArtifact(input)).toBe(expected); + }); + + it('should handle code fence with language specification', () => { + const input = "```typescript\n\n```"; + const expected = "\n\n"; + expect(stripCodeFenceFromArtifact(input)).toBe(expected); + }); + + it('should not modify content without artifacts', () => { + const input = '```\nregular code block\n```'; + expect(stripCodeFenceFromArtifact(input)).toBe(input); + }); + + it('should handle empty input', () => { + expect(stripCodeFenceFromArtifact('')).toBe(''); + }); + + it('should handle artifact without code fences', () => { + const input = ""; + expect(stripCodeFenceFromArtifact(input)).toBe(input); + }); + + it('should handle multiple artifacts but only remove fences around them', () => { + const input = [ + 'Some text', + '```typescript', + "", + '```', + '```', + 'regular code', + '```', + ].join('\n'); + + const expected = ['Some text', '', "", '', '```', 'regular code', '```'].join( + '\n', + ); + + expect(stripCodeFenceFromArtifact(input)).toBe(expected); + }); +}); diff --git a/app/components/chat/Markdown.tsx b/app/components/chat/Markdown.tsx index a91df43d..07b6a673 100644 --- a/app/components/chat/Markdown.tsx +++ b/app/components/chat/Markdown.tsx @@ -68,7 +68,51 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false remarkPlugins={remarkPlugins(limitedMarkdown)} rehypePlugins={rehypePlugins(html)} > - {children} + {stripCodeFenceFromArtifact(children)} ); }); + +/** + * Removes code fence markers (```) surrounding an artifact element while preserving the artifact content. + * This is necessary because artifacts should not be wrapped in code blocks when rendered for rendering action list. + * + * @param content - The markdown content to process + * @returns The processed content with code fence markers removed around artifacts + * + * @example + * // Removes code fences around artifact + * const input = "```xml\n\n```"; + * stripCodeFenceFromArtifact(input); + * // Returns: "\n\n" + * + * @remarks + * - Only removes code fences that directly wrap an artifact (marked with __boltArtifact__ class) + * - Handles code fences with optional language specifications (e.g. ```xml, ```typescript) + * - Preserves original content if no artifact is found + * - Safely handles edge cases like empty input or artifacts at start/end of content + */ +export const stripCodeFenceFromArtifact = (content: string) => { + if (!content || !content.includes('__boltArtifact__')) { + return content; + } + + const lines = content.split('\n'); + const artifactLineIndex = lines.findIndex((line) => line.includes('__boltArtifact__')); + + // Return original content if artifact line not found + if (artifactLineIndex === -1) { + return content; + } + + // Check previous line for code fence + if (artifactLineIndex > 0 && lines[artifactLineIndex - 1]?.trim().match(/^```\w*$/)) { + lines[artifactLineIndex - 1] = ''; + } + + if (artifactLineIndex < lines.length - 1 && lines[artifactLineIndex + 1]?.trim().match(/^```$/)) { + lines[artifactLineIndex + 1] = ''; + } + + return lines.join('\n'); +}; diff --git a/app/utils/logger.ts b/app/utils/logger.ts index 9b2c31c9..1a5c932c 100644 --- a/app/utils/logger.ts +++ b/app/utils/logger.ts @@ -11,7 +11,7 @@ interface Logger { setLevel: (level: DebugLevel) => void; } -let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info'; +let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info'; const isWorker = 'HTMLRewriter' in globalThis; const supportsColor = !isWorker;