@nuskin/chat-bot
Version:
React Chat Bot component for GenAI interaction with Amazon Bedrock
146 lines (137 loc) • 5.48 kB
JSX
import React, { useState, forwardRef, useEffect } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import rehypeRaw from 'rehype-raw'
import { Snackbar } from '@mui/material'
import MuiAlert from '@mui/material/Alert'
import './markdown.css'
const Alert = forwardRef(function Alert(props, ref) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
})
const CodeBlock = ({ children, className, rest }) => {
const [open, setOpen] = useState(false)
const [SyntaxHighlighter, setSyntaxHighlighter] = useState(null)
const [style, setStyle] = useState(null)
const [isClient, setIsClient] = useState(false)
const [hasNavigator, setHasNavigator] = useState(false)
useEffect(() => {
setIsClient(true)
setHasNavigator(typeof navigator !== 'undefined')
// Dynamically import syntax highlighter only on client side
Promise.all([
import('react-syntax-highlighter').then((mod) => setSyntaxHighlighter(() => mod.Prism)),
import('react-syntax-highlighter/dist/cjs/styles/prism').then((mod) => setStyle(mod.materialOceanic))
])
}, [])
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return
}
setOpen(false)
}
const match = /language-(\w+)/.exec(className || '')
const language = match ? match[1] : ''
// Only render syntax highlighter on client side and when loaded
if (!isClient || !SyntaxHighlighter || !style) {
return (
<code {...rest} className={className}>
{children}
</code>
)
}
return (
<>
{match ? (
<div className="relative">
{hasNavigator && (
<button
className="absolute top-2 right-2 py-1 px-2 bg-gray-300 rounded-md text-sm z-10 copy-btn"
onClick={(e) => {
e.preventDefault()
if (typeof navigator !== 'undefined') {
navigator.clipboard.writeText(children)
setOpen(true)
}
}}
>
Copy
</button>
)}
<SyntaxHighlighter style={style} language={language} PreTag="div">
{String(children)}
</SyntaxHighlighter>
<Snackbar
open={open}
autoHideDuration={6000}
data-testid="close-snackbar"
onClose={handleClose}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<Alert onClose={handleClose} data-testid="close-alert" severity="success" sx={{ width: '100%' }}>
Copied to clipboard!
</Alert>
</Snackbar>
</div>
) : (
<code {...rest} className={className}>
{children}
</code>
)}
</>
)
}
/**
* Render text as Markdown
*
* @param {object} params.tab - Markdown string
* @returns
*/
export default function RenderMarkdown({ tab }) {
return (
<Markdown
data-testid="markdown"
remarkPlugins={[remarkGfm, remarkBreaks]}
rehypePlugins={[rehypeRaw]}
components={{
code: CodeBlock,
h1: 'h2',
h2: 'h3',
h3: 'h4',
h4: 'h5',
h5: 'h6',
h6: 'h6',
ul: ({ children, node, ...props }) => {
// Check if this list is nested inside a list item
const isNested = node?.parent?.type === 'element' && node?.parent?.tagName === 'li'
return (
<ul className={`list-disc mb-3 ${isNested ? 'pl-10' : 'pl-5'}`} {...props}>
{children}
</ul>
)
},
ol: ({ children, node, ...props }) => {
// Check if this list is nested inside a list item
const isNested = node?.parent?.type === 'element' && node?.parent?.tagName === 'li'
return (
<ol className={`list-decimal mb-3 ${isNested ? 'pl-10' : 'pl-5'}`} {...props}>
{children}
</ol>
)
},
li: ({ children, node, ...props }) => {
// Check if the list item contains nested lists
const hasNestedList = node?.children?.some((child) => child.type === 'element' && ['ul', 'ol'].includes(child.tagName))
return (
<li className={`mb-1 ${hasNestedList ? 'list-item' : ''}`} {...props}>
{children}
</li>
)
},
p: ({ children }) => <p className="mb-3">{children}</p>
}}
>
{tab}
</Markdown>
)
}