'use client' import { memo, useMemo, useState, useCallback } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import ShikiHighlighter, { isInlineCode } from 'react-shiki' import type { Components } from 'react-markdown' import type { Element } from 'hast' import { repairStreamingMarkdown } from '../lib/streamingMarkdown' // Copy icon component function CopyIcon({ className }: { className?: string }) { return ( ) } function CheckIcon({ className }: { className?: string }) { return ( ) } // Language aliases for common variations const langAlias: Record = { js: 'javascript', ts: 'typescript', tsx: 'tsx', jsx: 'jsx', sh: 'bash', shell: 'bash', zsh: 'bash', yml: 'yaml', py: 'python', rb: 'ruby', rs: 'rust', go: 'go', md: 'markdown', mdx: 'mdx', vue: 'vue', svelte: 'svelte', dockerfile: 'docker', tf: 'hcl', terraform: 'hcl', cs: 'csharp', 'c#': 'csharp', 'c++': 'cpp', kt: 'kotlin', objc: 'objective-c', 'objective-c': 'objc', pl: 'perl', hs: 'haskell', ex: 'elixir', erl: 'erlang', fs: 'fsharp', 'f#': 'fsharp', ps1: 'powershell', psm1: 'powershell', } interface CodeBlockProps { className?: string children?: React.ReactNode node?: Element } function CodeBlock({ className, children, node, ...props }: CodeBlockProps) { const [copied, setCopied] = useState(false) const code = String(children).trim() const match = className?.match(/language-(\w+)/) let language = match ? match[1] : 'text' // Apply language aliases if (language in langAlias) { language = langAlias[language] } const isInline = node ? isInlineCode(node) : !code.includes('\n') const handleCopy = useCallback(async () => { await navigator.clipboard.writeText(code) setCopied(true) setTimeout(() => setCopied(false), 2000) }, [code]) if (isInline) { return ( {children} ) } return (
{code}
) } // Custom components for react-markdown const components: Components = { code: CodeBlock as Components['code'], // Style other markdown elements p: ({ children }) => (

{children}

), // Outside markers with padding keep wrapped list text aligned; task lists // (GFM `- [ ]`) drop the bullet in favor of their checkbox. ul: ({ children, className }) => ( ), ol: ({ children, start }) => (
    {children}
), li: ({ children }) => (
  • {children}
  • ), input: ({ checked, type }) => type === 'checkbox' ? ( ) : null, del: ({ children }) => ( {children} ), sup: ({ children }) => ( {children} ), section: ({ children, className }) => (
    {children}
    ), h1: ({ children }) => (

    {children}

    ), h2: ({ children }) => (

    {children}

    ), h3: ({ children }) => (

    {children}

    ), h4: ({ children }) => (

    {children}

    ), blockquote: ({ children }) => (
    {children}
    ), // External links open in a new tab; in-page anchors (GFM footnotes) don't. a: ({ href, children }) => { const external = !!href && !href.startsWith('#') return ( {children} ) }, strong: ({ children }) => ( {children} ), em: ({ children }) => ( {children} ), hr: () => (
    ), table: ({ children }) => (
    {children}
    ), th: ({ children, style }) => ( {children} ), td: ({ children, style }) => ( {children} ), } const remarkPlugins = [remarkGfm] interface MarkdownProps { children: string className?: string /** True while the message is still streaming; repairs partial syntax. */ streaming?: boolean } export const Markdown = memo(function Markdown({ children, className, streaming = false }: MarkdownProps) { const source = useMemo( () => (streaming ? repairStreamingMarkdown(children) : children), [children, streaming], ) return (
    {source}
    ) })