import { memo, useMemo } from 'react'; import ReactMarkdown, { type Components } from 'react-markdown'; import type { BundledLanguage } from 'shiki'; import { createScopedLogger } from '~/utils/logger'; import { rehypePlugins, remarkPlugins, allowedHTMLElements } from '~/utils/markdown'; import { Artifact, openArtifactInWorkbench } from './Artifact'; import { CodeBlock } from './CodeBlock'; import type { Message } from 'ai'; import styles from './Markdown.module.scss'; import ThoughtBox from './ThoughtBox'; import type { ProviderInfo } from '~/types/model'; const logger = createScopedLogger('MarkdownComponent'); interface MarkdownProps { children: string; html?: boolean; limitedMarkdown?: boolean; append?: (message: Message) => void; chatMode?: 'discuss' | 'build'; setChatMode?: (mode: 'discuss' | 'build') => void; model?: string; provider?: ProviderInfo; } export const Markdown = memo( ({ children, html = false, limitedMarkdown = false, append, setChatMode, model, provider }: MarkdownProps) => { logger.trace('Render'); const components = useMemo(() => { return { div: ({ className, children, node, ...props }) => { const dataProps = node?.properties as Record; if (className?.includes('__boltArtifact__')) { const messageId = node?.properties.dataMessageId as string; if (!messageId) { logger.error(`Invalid message id ${messageId}`); } return ; } if (className?.includes('__boltThought__')) { return {children}; } if (className?.includes('__boltQuickAction__') || dataProps?.dataBoltQuickAction) { return
{children}
; } return (
{children}
); }, pre: (props) => { const { children, node, ...rest } = props; const [firstChild] = node?.children ?? []; if ( firstChild && firstChild.type === 'element' && firstChild.tagName === 'code' && firstChild.children[0].type === 'text' ) { const { className, ...rest } = firstChild.properties; const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? []; return ; } return
{children}
; }, button: ({ node, children, ...props }) => { const dataProps = node?.properties as Record; if ( dataProps?.class?.toString().includes('__boltQuickAction__') || dataProps?.dataBoltQuickAction === 'true' ) { const type = dataProps['data-type'] || dataProps.dataType; const message = dataProps['data-message'] || dataProps.dataMessage; const path = dataProps['data-path'] || dataProps.dataPath; const href = dataProps['data-href'] || dataProps.dataHref; const iconClassMap: Record = { file: 'i-ph:file', message: 'i-ph:chats', implement: 'i-ph:code', link: 'i-ph:link', }; const safeType = typeof type === 'string' ? type : ''; const iconClass = iconClassMap[safeType] ?? 'i-ph:question'; return ( ); } return ; }, } satisfies Components; }, []); return ( {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'); };