mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-05-28 17:18:09 +08:00

Signed-off-by: yihong0618 <zouzou0208@gmail.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: xhe <xw897002528@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: takatost <takatost@gmail.com> Co-authored-by: kurokobo <kuro664@gmail.com> Co-authored-by: Novice Lee <novicelee@NoviPro.local> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: AkaraChen <akarachen@outlook.com> Co-authored-by: Yi <yxiaoisme@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com> Co-authored-by: AkaraChen <85140972+AkaraChen@users.noreply.github.com> Co-authored-by: NFish <douxc512@gmail.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Novice <857526207@qq.com> Co-authored-by: Hiroki Nagai <82458324+nagaihiroki-git@users.noreply.github.com> Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com> Co-authored-by: eux <euxuuu@gmail.com> Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com> Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com> Co-authored-by: lotsik <lotsik@mail.ru> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com> Co-authored-by: CN-P5 <heibai2006@gmail.com> Co-authored-by: CN-P5 <heibai2006@qq.com> Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Boris Feld <lothiraldan@gmail.com> Co-authored-by: mbo <himabo@gmail.com> Co-authored-by: mabo <mabo@aeyes.ai> Co-authored-by: Warren Chen <warren.chen830@gmail.com> Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com> Co-authored-by: jiandanfeng <chenjh3@wangsu.com> Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com> Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com> Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com> Co-authored-by: Xu Song <xusong.vip@gmail.com> Co-authored-by: rayshaw001 <396301947@163.com> Co-authored-by: Ding Jiatong <dingjiatong@gmail.com> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: JasonVV <jasonwangiii@outlook.com> Co-authored-by: le0zh <newlight@qq.com> Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com> Co-authored-by: k-zaku <zaku99@outlook.jp> Co-authored-by: luckylhb90 <luckylhb90@gmail.com> Co-authored-by: hobo.l <hobo.l@binance.com> Co-authored-by: jiangbo721 <365065261@qq.com> Co-authored-by: 刘江波 <jiangbo721@163.com> Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com> Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com> Co-authored-by: lowell <lowell.hu@zkteco.in> Co-authored-by: Boris Polonsky <BorisPolonsky@users.noreply.github.com> Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com> Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com> Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com> Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com> Co-authored-by: Jason <ggbbddjm@gmail.com> Co-authored-by: Xin Zhang <sjhpzx@gmail.com> Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com> Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com> Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com> Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com> Co-authored-by: Yingchun Lai <laiyingchun@apache.org> Co-authored-by: Hash Brown <hi@xzd.me> Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com> Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com> Co-authored-by: aplio <ryo.091219@gmail.com> Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com> Co-authored-by: Nam Vu <zuzoovn@gmail.com> Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com> Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com> Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com> Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com> Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp> Co-authored-by: HQidea <HQidea@users.noreply.github.com> Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com> Co-authored-by: xhe <xw897002528@gmail.com> Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com> Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com> Co-authored-by: engchina <12236799+engchina@users.noreply.github.com> Co-authored-by: engchina <atjapan2015@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: Kemal <kemalmeler@outlook.com> Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com> Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com> Co-authored-by: steven <sunzwj@digitalchina.com> Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com> Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com> Co-authored-by: 胡春东 <gycm520@gmail.com> Co-authored-by: Junjie.M <118170653@qq.com> Co-authored-by: MuYu <mr.muzea@gmail.com> Co-authored-by: Naoki Takashima <39912547+takatea@users.noreply.github.com> Co-authored-by: Summer-Gu <37869445+gubinjie@users.noreply.github.com> Co-authored-by: Fei He <droxer.he@gmail.com> Co-authored-by: ybalbert001 <120714773+ybalbert001@users.noreply.github.com> Co-authored-by: Yuanbo Li <ybalbert@amazon.com> Co-authored-by: douxc <7553076+douxc@users.noreply.github.com> Co-authored-by: liuzhenghua <1090179900@qq.com> Co-authored-by: Wu Jiayang <62842862+Wu-Jiayang@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: kimjion <45935338+kimjion@users.noreply.github.com> Co-authored-by: AugNSo <song.tiankai@icloud.com> Co-authored-by: llinvokerl <38915183+llinvokerl@users.noreply.github.com> Co-authored-by: liusurong.lsr <liusurong.lsr@alibaba-inc.com> Co-authored-by: Vasu Negi <vasu-negi@users.noreply.github.com> Co-authored-by: Hundredwz <1808096180@qq.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
332 lines
11 KiB
TypeScript
332 lines
11 KiB
TypeScript
import type { Components } from 'react-markdown'
|
|
import ReactMarkdown from 'react-markdown'
|
|
import ReactEcharts from 'echarts-for-react'
|
|
import 'katex/dist/katex.min.css'
|
|
import RemarkMath from 'remark-math'
|
|
import RemarkBreaks from 'remark-breaks'
|
|
import RehypeKatex from 'rehype-katex'
|
|
import RemarkGfm from 'remark-gfm'
|
|
import RehypeRaw from 'rehype-raw'
|
|
import SyntaxHighlighter from 'react-syntax-highlighter'
|
|
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 cn from '@/utils/classnames'
|
|
import CopyBtn from '@/app/components/base/copy-btn'
|
|
import SVGBtn from '@/app/components/base/svg'
|
|
import Flowchart from '@/app/components/base/mermaid'
|
|
import ImageGallery from '@/app/components/base/image-gallery'
|
|
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
|
import VideoGallery from '@/app/components/base/video-gallery'
|
|
import AudioGallery from '@/app/components/base/audio-gallery'
|
|
import SVGRenderer from '@/app/components/base/svg-gallery'
|
|
import MarkdownButton from '@/app/components/base/markdown-blocks/button'
|
|
import MarkdownForm from '@/app/components/base/markdown-blocks/form'
|
|
import type { ElementContentMap } from 'hast'
|
|
|
|
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
|
|
const capitalizationLanguageNameMap: Record<string, string> = {
|
|
sql: 'SQL',
|
|
javascript: 'JavaScript',
|
|
java: 'Java',
|
|
typescript: 'TypeScript',
|
|
vbscript: 'VBScript',
|
|
css: 'CSS',
|
|
html: 'HTML',
|
|
xml: 'XML',
|
|
php: 'PHP',
|
|
python: 'Python',
|
|
yaml: 'Yaml',
|
|
mermaid: 'Mermaid',
|
|
markdown: 'MarkDown',
|
|
makefile: 'MakeFile',
|
|
echarts: 'ECharts',
|
|
shell: 'Shell',
|
|
powershell: 'PowerShell',
|
|
json: 'JSON',
|
|
latex: 'Latex',
|
|
svg: 'SVG',
|
|
}
|
|
const getCorrectCapitalizationLanguageName = (language: string) => {
|
|
if (!language)
|
|
return 'Plain'
|
|
|
|
if (language in capitalizationLanguageNameMap)
|
|
return capitalizationLanguageNameMap[language]
|
|
|
|
return language.charAt(0).toUpperCase() + language.substring(1)
|
|
}
|
|
|
|
const preprocessLaTeX = (content?: string) => {
|
|
if (typeof content !== 'string')
|
|
return content
|
|
|
|
return flow([
|
|
(str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`),
|
|
(str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`),
|
|
(str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`),
|
|
])(content)
|
|
}
|
|
|
|
const preprocessThinkTag = (content: string) => {
|
|
if (!content.trim().startsWith('<think>\n'))
|
|
return content
|
|
|
|
return flow([
|
|
(str: string) => str.replace('<think>\n', '<details>\n'),
|
|
(str: string) => str.replace('\n</think>', '\n[ENDTHINKFLAG]</details>'),
|
|
])(content)
|
|
}
|
|
|
|
export function PreCode(props: { children: any }) {
|
|
const ref = useRef<HTMLPreElement>(null)
|
|
|
|
return (
|
|
<pre ref={ref}>
|
|
<span
|
|
className="copy-code-button"
|
|
></span>
|
|
{props.children}
|
|
</pre>
|
|
)
|
|
}
|
|
|
|
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
|
|
// Avoid error #185 (Maximum update depth exceeded.
|
|
// This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
|
|
// React limits the number of nested updates to prevent infinite loops.)
|
|
// Reference A: https://reactjs.org/docs/error-decoder.html?invariant=185
|
|
// Reference B1: https://react.dev/reference/react/memo
|
|
// Reference B2: https://react.dev/reference/react/useMemo
|
|
// ****
|
|
// The original error that occurred in the streaming response during the conversation:
|
|
// Error: Minified React error 185;
|
|
// 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.
|
|
|
|
const CodeBlock: Components['code'] = memo(({ className, children, ...props }) => {
|
|
const { inline } = useContext(PreContext)
|
|
const [isSVG, setIsSVG] = useState(true)
|
|
const match = /language-(\w+)/.exec(className || '')
|
|
const language = match?.[1]
|
|
const languageShowName = getCorrectCapitalizationLanguageName(language || '')
|
|
const chartData = useMemo(() => {
|
|
if (language === 'echarts') {
|
|
try {
|
|
return JSON.parse(String(children).replace(/\n$/, ''))
|
|
}
|
|
catch (error) { }
|
|
}
|
|
return JSON.parse('{"title":{"text":"ECharts error - Wrong JSON format."}}')
|
|
}, [language, children])
|
|
|
|
const renderCodeContent = useMemo(() => {
|
|
const content = String(children).replace(/\n$/, '')
|
|
if (language === 'mermaid' && isSVG) {
|
|
return <Flowchart PrimitiveCode={content} />
|
|
}
|
|
else if (language === 'echarts') {
|
|
return (
|
|
<div style={{ minHeight: '350px', minWidth: '100%', overflowX: 'scroll' }}>
|
|
<ErrorBoundary>
|
|
<ReactEcharts option={chartData} style={{ minWidth: '700px' }} />
|
|
</ErrorBoundary>
|
|
</div>
|
|
)
|
|
}
|
|
else if (language === 'svg' && isSVG) {
|
|
return (
|
|
<ErrorBoundary>
|
|
<SVGRenderer content={content} />
|
|
</ErrorBoundary>
|
|
)
|
|
}
|
|
else {
|
|
return (
|
|
<SyntaxHighlighter
|
|
{...props as any}
|
|
style={atelierHeathLight}
|
|
customStyle={{
|
|
paddingLeft: 12,
|
|
backgroundColor: '#fff',
|
|
}}
|
|
language={match?.[1]}
|
|
showLineNumbers
|
|
PreTag="div"
|
|
>
|
|
{content}
|
|
</SyntaxHighlighter>
|
|
)
|
|
}
|
|
}, [language, match, props, children, chartData, isSVG])
|
|
|
|
if (inline || !match)
|
|
return <code {...props} className={className}>{children}</code>
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className='flex justify-between h-8 items-center p-1 pl-3 border-b'
|
|
style={{
|
|
borderColor: 'rgba(0, 0, 0, 0.05)',
|
|
}}
|
|
>
|
|
<div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
|
|
<div style={{ display: 'flex' }}>
|
|
{(['mermaid', 'svg']).includes(language!) && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />}
|
|
<CopyBtn
|
|
className='mr-1'
|
|
value={String(children).replace(/\n$/, '')}
|
|
isPlain
|
|
/>
|
|
</div>
|
|
</div>
|
|
{renderCodeContent}
|
|
</div>
|
|
)
|
|
})
|
|
// 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)
|
|
if (srcs.length === 0)
|
|
return null
|
|
return <VideoGallery key={srcs.join()} srcs={srcs} />
|
|
})
|
|
// 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)
|
|
if (srcs.length === 0)
|
|
return null
|
|
return <AudioGallery key={srcs.join()} srcs={srcs} />
|
|
})
|
|
// AudioBlock.displayName = 'AudioBlock'
|
|
|
|
const ScriptBlock = memo(({ node }: any) => {
|
|
const scriptContent = node.children[0]?.value || ''
|
|
return `<script>${scriptContent}</script>`
|
|
})
|
|
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 Img: Components['img'] = ({ src }) => {
|
|
return (<ImageGallery srcs={[src!]} />)
|
|
}
|
|
|
|
const Link: Components['a'] = ({ node, ...props }) => {
|
|
if (node!.properties?.href && node!.properties.href?.toString().startsWith('abbr')) {
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
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>
|
|
}
|
|
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>
|
|
}
|
|
}
|
|
|
|
export function Markdown(props: { content: string; className?: string }) {
|
|
const latexContent = flow([
|
|
preprocessThinkTag,
|
|
preprocessLaTeX,
|
|
])(props.content)
|
|
return (
|
|
<div className={cn('markdown-body', props.className)}>
|
|
<ReactMarkdown
|
|
remarkPlugins={[
|
|
RemarkGfm,
|
|
[RemarkMath, { singleDollarTextMath: false }],
|
|
RemarkBreaks,
|
|
]}
|
|
rehypePlugins={[
|
|
RehypeKatex,
|
|
RehypeRaw as any,
|
|
// The Rehype plug-in is used to remove the ref attribute of an element
|
|
() => {
|
|
return (tree) => {
|
|
const iterate = (node: any) => {
|
|
if (node.type === 'element' && node.properties?.ref)
|
|
delete node.properties.ref
|
|
|
|
if (node.children)
|
|
node.children.forEach(iterate)
|
|
}
|
|
tree.children.forEach(iterate)
|
|
}
|
|
},
|
|
]}
|
|
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body']}
|
|
components={{
|
|
pre: PreBlock,
|
|
code: CodeBlock,
|
|
img: Img,
|
|
video: VideoBlock,
|
|
audio: AudioBlock,
|
|
a: Link,
|
|
p: Paragraph,
|
|
button: MarkdownButton,
|
|
form: MarkdownForm,
|
|
script: ScriptBlock,
|
|
details: ThinkBlock,
|
|
}}
|
|
>
|
|
{/* Markdown detect has problem. */}
|
|
{latexContent}
|
|
</ReactMarkdown>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// **Add an ECharts runtime error handler
|
|
// Avoid error #7832 (Crash when ECharts accesses undefined objects)
|
|
// This can happen when a component attempts to access an undefined object that references an unregistered map, causing the program to crash.
|
|
|
|
export default class ErrorBoundary extends Component {
|
|
constructor(props: any) {
|
|
super(props)
|
|
this.state = { hasError: false }
|
|
}
|
|
|
|
componentDidCatch(error: any, errorInfo: any) {
|
|
this.setState({ hasError: true })
|
|
console.error(error, errorInfo)
|
|
}
|
|
|
|
render() {
|
|
// eslint-disable-next-line ts/ban-ts-comment
|
|
// @ts-expect-error
|
|
if (this.state.hasError)
|
|
return <div>Oops! An error occurred. This could be due to an ECharts runtime error or invalid SVG content. <br />(see the browser console for more information)</div>
|
|
// eslint-disable-next-line ts/ban-ts-comment
|
|
// @ts-expect-error
|
|
return this.props.children
|
|
}
|
|
}
|