From a430284aa21e3ae1f0d5654e55b2ad2852519cc2 Mon Sep 17 00:00:00 2001 From: wwf <yearningwang@iqtogether.com> Date: 星期三, 04 六月 2025 15:17:49 +0800 Subject: [PATCH] 初始化 --- app/components/base/markdown.tsx | 218 ++++++++++++++++++++---------------------------------- 1 files changed, 81 insertions(+), 137 deletions(-) diff --git a/app/components/base/markdown.tsx b/app/components/base/markdown.tsx index bc6fe0e..b26d9df 100644 --- a/app/components/base/markdown.tsx +++ b/app/components/base/markdown.tsx @@ -7,28 +7,20 @@ import RemarkGfm from 'remark-gfm' import RehypeRaw from 'rehype-raw' import SyntaxHighlighter from 'react-syntax-highlighter' -import { - atelierHeathDark, - atelierHeathLight, -} from 'react-syntax-highlighter/dist/esm/styles/hljs' +import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs' import { Component, memo, useMemo, useRef, useState } from 'react' -import { flow } from 'lodash-es' -import ActionButton from '@/app/components/base/action-button' -import CopyIcon from '@/app/components/base/copy-icon' +import type { CodeComponent } from 'react-markdown/lib/ast-to-react' +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 MarkdownMusic from '@/app/components/base/markdown-blocks/music' -import ThinkBlock from '@/app/components/base/markdown-blocks/think-block' -import { Theme } from '@/types/app' -import useTheme from '@/hooks/use-theme' -import cn from '@/utils/classnames' -import SVGRenderer from './svg-gallery' // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD const capitalizationLanguageNameMap: Record<string, string> = { @@ -52,7 +44,6 @@ json: 'JSON', latex: 'Latex', svg: 'SVG', - abc: 'ABC', } const getCorrectCapitalizationLanguageName = (language: string) => { if (!language) @@ -67,32 +58,9 @@ const preprocessLaTeX = (content: string) => { if (typeof content !== 'string') return content - - const codeBlockRegex = /```[\s\S]*?```/g - const codeBlocks = content.match(codeBlockRegex) || [] - let processedContent = content.replace(codeBlockRegex, 'CODE_BLOCK_PLACEHOLDER') - - processedContent = flow([ - (str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`), - (str: string) => str.replace(/\\\[(.*?)\\\]/gs, (_, equation) => `$$${equation}$$`), - (str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`), - (str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`), - ])(processedContent) - - codeBlocks.forEach((block) => { - processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', block) - }) - - return processedContent -} - -const preprocessThinkTag = (content: string) => { - const thinkOpenTagRegex = /<think>\n/g - const thinkCloseTagRegex = /\n<\/think>/g - return flow([ - (str: string) => str.replace(thinkOpenTagRegex, '<details data-think=true>\n'), - (str: string) => str.replace(thinkCloseTagRegex, '\n[ENDTHINKFLAG]</details>'), - ])(content) + return content.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`) + .replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`) + .replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`) } export function PreCode(props: { children: any }) { @@ -121,91 +89,80 @@ // 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: any = memo(({ inline, className, children = '', ...props }: any) => { - const { theme } = useTheme() +const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }) => { const [isSVG, setIsSVG] = useState(true) const match = /language-(\w+)/.exec(className || '') const language = match?.[1] const languageShowName = getCorrectCapitalizationLanguageName(language || '') const chartData = useMemo(() => { - const str = String(children).replace(/\n$/, '') if (language === 'echarts') { try { - return JSON.parse(str) + return JSON.parse(String(children).replace(/\n$/, '')) } - catch { } - try { - // eslint-disable-next-line no-new-func, sonarjs/code-eval - return new Function(`return ${str}`)() - } - catch { } + catch (error) { } } - return JSON.parse('{"title":{"text":"ECharts error - Wrong option."}}') + return JSON.parse('{"title":{"text":"ECharts error - Wrong JSON format."}}') }, [language, children]) const renderCodeContent = useMemo(() => { const content = String(children).replace(/\n$/, '') - switch (language) { - case 'mermaid': - if (isSVG) - return <Flowchart PrimitiveCode={content} /> - break - case 'echarts': - return ( - <div style={{ minHeight: '350px', minWidth: '100%', overflowX: 'scroll' }}> - <ErrorBoundary> - <ReactEcharts option={chartData} style={{ minWidth: '700px' }} /> - </ErrorBoundary> - </div> - ) - case 'svg': - if (isSVG) { - return ( - <ErrorBoundary> - <SVGRenderer content={content} /> - </ErrorBoundary> - ) - } - break - case 'abc': - return ( - <ErrorBoundary> - <MarkdownMusic children={content} /> - </ErrorBoundary> - ) - default: - return ( - <SyntaxHighlighter - {...props} - style={theme === Theme.light ? atelierHeathLight : atelierHeathDark} - customStyle={{ - paddingLeft: 12, - borderBottomLeftRadius: '10px', - borderBottomRightRadius: '10px', - backgroundColor: 'var(--color-components-input-bg-normal)', - }} - language={match?.[1]} - showLineNumbers - PreTag="div" - > - {content} - </SyntaxHighlighter> - ) + if (language === 'mermaid' && isSVG) { + return <Flowchart PrimitiveCode={content} /> } - }, [children, language, isSVG, chartData, props, theme, match]) + 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} + 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 className='relative'> - <div className='flex h-8 items-center justify-between rounded-t-[10px] border-b border-divider-subtle bg-components-input-bg-normal p-1 pl-3'> - <div className='system-xs-semibold-uppercase text-text-secondary'>{languageShowName}</div> - <div className='flex items-center gap-1'> + <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} />} - <ActionButton> - <CopyIcon content={String(children).replace(/\n$/, '')} /> - </ActionButton> + <CopyBtn + className='mr-1' + value={String(children).replace(/\n$/, '')} + isPlain + /> </div> </div> {renderCodeContent} @@ -214,16 +171,16 @@ }) CodeBlock.displayName = 'CodeBlock' -const VideoBlock: any = memo(({ node }: any) => { - const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src) +const VideoBlock: CodeComponent = 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: any = memo(({ node }: any) => { - const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src) +const AudioBlock: CodeComponent = 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} /> @@ -241,44 +198,36 @@ const children_node = node.children if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') { return ( - <div className="markdown-img-wrapper"> + <> <ImageGallery srcs={[children_node[0].properties.src]} /> - { - Array.isArray(paragraph.children) && paragraph.children.length > 1 && ( - <div className="mt-2">{paragraph.children.slice(1)}</div> - ) - } - </div> + <p>{paragraph.children.slice(1)}</p> + </> ) } return <p>{paragraph.children}</p> } const Img = ({ src }: any) => { - return <div className="markdown-img-wrapper"><ImageGallery srcs={[src]} /></div> + return (<ImageGallery srcs={[src]} />) } -const Link = ({ node, children, ...props }: any) => { +const Link = ({ node, ...props }: any) => { 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]) - return <abbr className="cursor-pointer underline !decoration-primary-700 decoration-dashed" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr> + 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 { - return <a {...props} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || '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> } } -export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) { - const latexContent = flow([ - preprocessThinkTag, - preprocessLaTeX, - ])(props.content) - +export function Markdown(props: { content: string; className?: string }) { + const latexContent = preprocessLaTeX(props.content) return ( - <div className={cn('markdown-body', '!text-text-primary', props.className)}> + <div className={cn(props.className, 'markdown-body')}> <ReactMarkdown remarkPlugins={[ RemarkGfm, @@ -295,11 +244,6 @@ if (node.type === 'element' && node.properties?.ref) delete node.properties.ref - if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) { - node.type = 'text' - node.value = `<${node.tagName}` - } - if (node.children) node.children.forEach(iterate) } @@ -307,7 +251,7 @@ } }, ]} - disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]} + disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body']} components={{ code: CodeBlock, img: Img, @@ -317,9 +261,9 @@ p: Paragraph, button: MarkdownButton, form: MarkdownForm, - script: ScriptBlock as any, - details: ThinkBlock, + script: ScriptBlock, }} + linkTarget='_blank' > {/* Markdown detect has problem. */} {latexContent} @@ -344,11 +288,11 @@ } render() { - // eslint-disable-next-line ts/ban-ts-comment + // eslint-disable-next-line @typescript-eslint/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 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error return this.props.children } -- Gitblit v1.8.0