Spaces:
Sleeping
Sleeping
| 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 } from './Artifact'; | |
| import { CodeBlock } from './CodeBlock'; | |
| import styles from './Markdown.module.scss'; | |
| import ThoughtBox from './ThoughtBox'; | |
| const logger = createScopedLogger('MarkdownComponent'); | |
| interface MarkdownProps { | |
| children: string; | |
| html?: boolean; | |
| limitedMarkdown?: boolean; | |
| } | |
| export const Markdown = memo(({ children, html = false, limitedMarkdown = false }: MarkdownProps) => { | |
| logger.trace('Render'); | |
| const components = useMemo(() => { | |
| return { | |
| div: ({ className, children, node, ...props }) => { | |
| if (className?.includes('__boltArtifact__')) { | |
| const messageId = node?.properties.dataMessageId as string; | |
| if (!messageId) { | |
| logger.error(`Invalid message id ${messageId}`); | |
| } | |
| return <Artifact messageId={messageId} />; | |
| } | |
| if (className?.includes('__boltThought__')) { | |
| return <ThoughtBox title="Thought process">{children}</ThoughtBox>; | |
| } | |
| return ( | |
| <div className={className} {...props}> | |
| {children} | |
| </div> | |
| ); | |
| }, | |
| 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 <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />; | |
| } | |
| return <pre {...rest}>{children}</pre>; | |
| }, | |
| } satisfies Components; | |
| }, []); | |
| return ( | |
| <ReactMarkdown | |
| allowedElements={allowedHTMLElements} | |
| className={styles.MarkdownContent} | |
| components={components} | |
| remarkPlugins={remarkPlugins(limitedMarkdown)} | |
| rehypePlugins={rehypePlugins(html)} | |
| > | |
| {stripCodeFenceFromArtifact(children)} | |
| </ReactMarkdown> | |
| ); | |
| }); | |
| /** | |
| * 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<div class='__boltArtifact__'></div>\n```"; | |
| * stripCodeFenceFromArtifact(input); | |
| * // Returns: "\n<div class='__boltArtifact__'></div>\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'); | |
| }; | |