'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 }) => (