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