通用技术 Mermaid 在 React Markdown 组件中的应用

JoyMao · 2023年09月27日 · 4761 次阅读

这个是测试工具中的一个完成 KPI 任务的功能...
目的是在测试工具中提供一个共享 AI Prompts 的功能,大家可以在这里分享自己好的 prompts,即查即用,抹平使用 chatGPT 帐号、网络等因素。

这里需要解决一个问题就是 AI 接口响应的 mermaid 代码能渲染成对应图表功能:
react 前端都是依葫芦画瓢 (我对 vue 更熟悉),这次遇到很多坑,而且这个平台的 react、antd 版本有点老了,找到合适的组件版本非常费时间。

实现的效果是 ai 返回的 markdown 内容中如果有 mermaid 代码,则提供渲染按钮,点击后渲染出对应的图表.
这个是取巧的做法,另一种做法是直接替换 mermaid 代码块,但这样破坏了 ai 接口响应的内容且不够美观;

  • 一些避坑方法供参考:

1、直接从响应文本中匹配 mermaid 代码的正则很难写的完善:等 markdown 的组件中获取到 mermaid 代码段中的代码则不会有太多的异常情况。
这里用 useState, useEffect 处理等待组件渲染完成处理
2、承接上个处理:markdown 中 code 段有时会有行号,从 code 中取 innerText 需要清理掉行号,所以利用replaceAll(/(?<=^|\n|\r)\d+/g,'')即可
3、另外 mermaid 的这个 react 版本遇到中文符号会报错,一种方法是把所有文本用""包裹(情况多不好处理),另一种方法就是转换为为没有;结尾 html 实体码,使用利用正则匹配处理即可replaceAll( /\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5/g,
function(m){
return "&#" + m.charCodeAt()
}
)

代码参考(实现了代码高亮及 mermaid 代码提供渲染功能)

import React, { useState, useEffect } from "react";
import {Modal,Button } from 'antd'
import ReactMarkdown from 'react-markdown';
import { toPng } from 'html-to-image';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import styles from './index.less';
import Mermaid from './Mermaid'

function AiRstComponent({aiRst,onAiRstComponentDone,showCodeLineNum}){
  useEffect(() => {
    onAiRstComponentDone();
  }, []);
  return (
    <ReactMarkdown
      plugins={[remarkGfm]}
      className={styles.mdTable}
      components={{
        code({ node, inline, className, children, ...props }) {
          const match = /language-(\w+)/.exec(className || '');
          return !inline && match ? (
            <SyntaxHighlighter
              showLineNumbers={showCodeLineNum}
              style={oneDark}
              language={match[1]}
              PreTag='div'
              {...props}
            >
              {String(children).replace(/\n$/, '')}
            </SyntaxHighlighter>
          ) : (
            <code className={className} {...props}>
              {children}
            </code>
          );
        },
      }}
    >
      {aiRst}
    </ReactMarkdown>
  )
};

function replaceCnMark(s){
// 针对mermaid报错,替换中文符号为实体码,但没有;号结尾
  return s.replaceAll( /\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5/g,
    function(m){
      return "&#" + m.charCodeAt()
    }
  )
}
function MarkDown({aiResult,mermaidModalState,handleMaidModalState,handleSaveImg,showCodeLineNum}) {
  const [codeStrList, setCodeStrList] = useState(null);
  const handleMarkDownMainDone = () => {
    // 当ComponentA完成渲染时,将componentADone状态设为true
    setMermaidcode();
  };
  const setMermaidcode= ()=>{
    const tmp=[]
    const merMaidCodeElems=document.querySelectorAll(".language-mermaid")
    if(merMaidCodeElems!==null && merMaidCodeElems.length>0){
      for(const codeElm of merMaidCodeElems){
        let codeStr=codeElm.innerText
        if(showCodeLineNum){
          codeStr=codeStr.replaceAll(/(?<=^|\n|\r)\d+/g,'')
        }
        tmp.push(replaceCnMark(codeStr)) //去除行号
      }
      setCodeStrList(tmp)
    }
  }
  return (
    <div>
      <div>
        <AiRstComponent
          aiRst={aiResult}
          onAiRstComponentDone={handleMarkDownMainDone}
          showCodeLineNum={showCodeLineNum}
        />
        {codeStrList?.length>0?
          <a onClick={() => handleMaidModalState(true)}>查看第1个mermaid代码渲染图</a>
          :null
        }

      </div>
      <Modal
        title="查看mermaid代码渲染图"
        width='60%'
        className={styles.mermaidChart}
        visible={mermaidModalState}
        onCancel={() => handleMaidModalState(false)}
        footer={[
          <Button key="submit" type="primary" onClick={() => handleSaveImg()}>
            Save
          </Button>,
          <Button onClick={() => handleMaidModalState(false)}>
            Close
          </Button>,
         ]}
      >
        <Mermaid chart={codeStrList?.length>0?codeStrList[0]:''} />
      </Modal>
    </div>
  );
}

export default class MarkDownComponent extends React.Component {
  state ={
    mermaidModalState:false,
    showCodeLineNum:true,
  }

  handleMaidModalState=(state)=>{
    this.setState({
      mermaidModalState:state,
    })
  }
  handleSaveImg=()=>{ //提供保存图表的方法
    toPng(document.querySelector('.mermaid svg')).then(
       (dataUrl) => {
        const link = document.createElement('a')
        link.download = 'mermaid_img.png'
        link.href = dataUrl
        link.click()
  });
  }

  render() {
    return(
      <MarkDown
        aiResult={this.props.aiResult}
        mermaidModalState={this.state.mermaidModalState}
        handleMaidModalState={this.handleMaidModalState}
        handleSaveImg={this.handleSaveImg}
        showCodeLineNum={this.state.showCodeLineNum}
      />
  )}
}

另外还有个 Mermaid 组件,直接照抄其官网例子:

import React from "react";
import mermaid from "mermaid";
mermaid.initialize({
  startOnLoad: true,
  theme: "default",
  securityLevel: "loose",
 });

export default class Mermaid extends React.Component {
  componentDidMount() {
    mermaid.contentLoaded();
  }
  render() {
    return <div className="mermaid" style={{textAlign:'center',overflow:'auto'}}>{this.props.chart}</div>;
  }
}
  • 上述呈现效果: 完善好 prompts 模板,填入需求规则

调用 AI 接口获取响应内容

渲染 mermaid 图

  • 补充:

上面的方法只渲染第 1 个 mermaid 的图,如果能要渲染 markdown 中多个图表,需要优化

后来想了想,其实可以直接在 markdown 的内容中显示图表
这个方法其实更加简单,在以下地方加上段代码即可,其他等待组件渲染处理、弹框啊等等代码都可以删掉。
这不过这样原来的 mermaid 代码就不显示,而且生成的图片很大情况下不够美观。


再次优化:
可以上述将 markdown 中 code 中判断 mermaid 后return <Mermaid/>组件再次封装一个组件,这个组件中提供 mermaid 代码和点击查看其对应图表功能。
这样就更加完美了。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册