UNPKG

@nuskin/chat-bot

Version:

React Chat Bot component for GenAI interaction with Amazon Bedrock

146 lines (137 loc) 5.48 kB
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> ) }