| import type { Components } from 'react-markdown' | |||||
| import ReactMarkdown from 'react-markdown' | import ReactMarkdown from 'react-markdown' | ||||
| import ReactEcharts from 'echarts-for-react' | import ReactEcharts from 'echarts-for-react' | ||||
| import 'katex/dist/katex.min.css' | import 'katex/dist/katex.min.css' | ||||
| import RehypeRaw from 'rehype-raw' | import RehypeRaw from 'rehype-raw' | ||||
| import SyntaxHighlighter from 'react-syntax-highlighter' | import SyntaxHighlighter from 'react-syntax-highlighter' | ||||
| import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs' | import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs' | ||||
| import { Component, createContext, memo, useContext, useMemo, useRef, useState } from 'react' | |||||
| import { flow } from 'lodash/fp' | |||||
| import { Component, memo, useMemo, useRef, useState } from 'react' | |||||
| import { flow } from 'lodash-es' | |||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import CopyBtn from '@/app/components/base/copy-btn' | import CopyBtn from '@/app/components/base/copy-btn' | ||||
| import SVGBtn from '@/app/components/base/svg' | import SVGBtn from '@/app/components/base/svg' | ||||
| import SVGRenderer from '@/app/components/base/svg-gallery' | import SVGRenderer from '@/app/components/base/svg-gallery' | ||||
| import MarkdownButton from '@/app/components/base/markdown-blocks/button' | import MarkdownButton from '@/app/components/base/markdown-blocks/button' | ||||
| import MarkdownForm from '@/app/components/base/markdown-blocks/form' | import MarkdownForm from '@/app/components/base/markdown-blocks/form' | ||||
| import type { ElementContentMap } from 'hast' | |||||
| import ThinkBlock from '@/app/components/base/markdown-blocks/think-block' | |||||
| // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD | // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD | ||||
| const capitalizationLanguageNameMap: Record<string, string> = { | const capitalizationLanguageNameMap: Record<string, string> = { | ||||
| return language.charAt(0).toUpperCase() + language.substring(1) | return language.charAt(0).toUpperCase() + language.substring(1) | ||||
| } | } | ||||
| const preprocessLaTeX = (content?: string) => { | |||||
| const preprocessLaTeX = (content: string) => { | |||||
| if (typeof content !== 'string') | if (typeof content !== 'string') | ||||
| return content | return content | ||||
| ) | ) | ||||
| } | } | ||||
| const PreContext = createContext({ | |||||
| // if children not in PreContext, just leave inline true | |||||
| inline: true, | |||||
| }) | |||||
| const PreBlock: Components['pre'] = (props) => { | |||||
| const { ...rest } = props | |||||
| return <PreContext.Provider value={{ | |||||
| inline: false, | |||||
| }}> | |||||
| <pre {...rest} /> | |||||
| </PreContext.Provider> | |||||
| } | |||||
| // **Add code block | // **Add code block | ||||
| // Avoid error #185 (Maximum update depth exceeded. | // Avoid error #185 (Maximum update depth exceeded. | ||||
| // This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. | // This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. | ||||
| // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message | // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message | ||||
| // or use the non-minified dev environment for full errors and additional helpful warnings. | // or use the non-minified dev environment for full errors and additional helpful warnings. | ||||
| const CodeBlock: Components['code'] = memo(({ className, children, ...props }) => { | |||||
| const { inline } = useContext(PreContext) | |||||
| const CodeBlock: any = memo(({ inline, className, children, ...props }) => { | |||||
| const [isSVG, setIsSVG] = useState(true) | const [isSVG, setIsSVG] = useState(true) | ||||
| const match = /language-(\w+)/.exec(className || '') | const match = /language-(\w+)/.exec(className || '') | ||||
| const language = match?.[1] | const language = match?.[1] | ||||
| else { | else { | ||||
| return ( | return ( | ||||
| <SyntaxHighlighter | <SyntaxHighlighter | ||||
| {...props as any} | |||||
| {...props} | |||||
| style={atelierHeathLight} | style={atelierHeathLight} | ||||
| customStyle={{ | customStyle={{ | ||||
| paddingLeft: 12, | paddingLeft: 12, | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| }) | }) | ||||
| // CodeBlock.displayName = 'CodeBlock' | |||||
| CodeBlock.displayName = 'CodeBlock' | |||||
| const VideoBlock: Components['video'] = memo(({ node }) => { | |||||
| const srcs = node!.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) | |||||
| const VideoBlock: any = memo(({ node }) => { | |||||
| const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) | |||||
| if (srcs.length === 0) | if (srcs.length === 0) | ||||
| return null | return null | ||||
| return <VideoGallery key={srcs.join()} srcs={srcs} /> | return <VideoGallery key={srcs.join()} srcs={srcs} /> | ||||
| }) | }) | ||||
| // VideoBlock.displayName = 'VideoBlock' | |||||
| VideoBlock.displayName = 'VideoBlock' | |||||
| const AudioBlock: Components['audio'] = memo(({ node }) => { | |||||
| const srcs = node!.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) | |||||
| const AudioBlock: any = memo(({ node }) => { | |||||
| const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) | |||||
| if (srcs.length === 0) | if (srcs.length === 0) | ||||
| return null | return null | ||||
| return <AudioGallery key={srcs.join()} srcs={srcs} /> | return <AudioGallery key={srcs.join()} srcs={srcs} /> | ||||
| }) | }) | ||||
| // AudioBlock.displayName = 'AudioBlock' | |||||
| AudioBlock.displayName = 'AudioBlock' | |||||
| const ScriptBlock = memo(({ node }: any) => { | const ScriptBlock = memo(({ node }: any) => { | ||||
| const scriptContent = node.children[0]?.value || '' | const scriptContent = node.children[0]?.value || '' | ||||
| }) | }) | ||||
| ScriptBlock.displayName = 'ScriptBlock' | ScriptBlock.displayName = 'ScriptBlock' | ||||
| const Paragraph: Components['p'] = ({ node, children }) => { | |||||
| const children_node = node!.children | |||||
| if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') | |||||
| return <ImageGallery srcs={[children_node?.[0]?.properties?.src as string]} /> | |||||
| return <p>{children}</p> | |||||
| const Paragraph = (paragraph: any) => { | |||||
| const { node }: any = paragraph | |||||
| const children_node = node.children | |||||
| if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') { | |||||
| return ( | |||||
| <> | |||||
| <ImageGallery srcs={[children_node[0].properties.src]} /> | |||||
| <p>{paragraph.children.slice(1)}</p> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| return <p>{paragraph.children}</p> | |||||
| } | } | ||||
| const Img: Components['img'] = ({ src }) => { | |||||
| return (<ImageGallery srcs={[src!]} />) | |||||
| const Img = ({ src }: any) => { | |||||
| return (<ImageGallery srcs={[src]} />) | |||||
| } | } | ||||
| const Link: Components['a'] = ({ node, ...props }) => { | |||||
| if (node!.properties?.href && node!.properties.href?.toString().startsWith('abbr')) { | |||||
| const Link = ({ node, ...props }: any) => { | |||||
| if (node.properties?.href && node.properties.href?.toString().startsWith('abbr')) { | |||||
| // eslint-disable-next-line react-hooks/rules-of-hooks | // eslint-disable-next-line react-hooks/rules-of-hooks | ||||
| const { onSend } = useChatContext() | const { onSend } = useChatContext() | ||||
| const hidden_text = decodeURIComponent(node!.properties.href.toString().split('abbr:')[1]) | |||||
| const title = (node!.children[0] as ElementContentMap['text'])?.value | |||||
| return <abbr className="underline decoration-dashed !decoration-primary-700 cursor-pointer" onClick={() => onSend?.(hidden_text)} title={title}>{title}</abbr> | |||||
| const hidden_text = decodeURIComponent(node.properties.href.toString().split('abbr:')[1]) | |||||
| return <abbr className="underline decoration-dashed !decoration-primary-700 cursor-pointer" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value}>{node.children[0]?.value}</abbr> | |||||
| } | } | ||||
| else { | else { | ||||
| const firstChild = node?.children?.[0] as ElementContentMap['text'] | undefined | |||||
| return <a {...props} target="_blank" className="underline decoration-dashed !decoration-primary-700 cursor-pointer">{ | |||||
| firstChild | |||||
| ? firstChild.value | |||||
| : 'Download' | |||||
| }</a> | |||||
| return <a {...props} target="_blank" className="underline decoration-dashed !decoration-primary-700 cursor-pointer">{node.children[0] ? node.children[0]?.value : 'Download'}</a> | |||||
| } | } | ||||
| } | } | ||||
| preprocessLaTeX, | preprocessLaTeX, | ||||
| ])(props.content) | ])(props.content) | ||||
| return ( | return ( | ||||
| <div className={cn('markdown-body', props.className)}> | |||||
| <div className={cn(props.className, 'markdown-body')}> | |||||
| <ReactMarkdown | <ReactMarkdown | ||||
| remarkPlugins={[ | remarkPlugins={[ | ||||
| RemarkGfm, | RemarkGfm, | ||||
| ]} | ]} | ||||
| disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body']} | disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body']} | ||||
| components={{ | components={{ | ||||
| pre: PreBlock, | |||||
| code: CodeBlock, | code: CodeBlock, | ||||
| img: Img, | img: Img, | ||||
| video: VideoBlock, | video: VideoBlock, |