/ easyshell-web / src / components / MarkdownContent.tsx
MarkdownContent.tsx
 1  import React, { useState, useRef } from 'react';
 2  import ReactMarkdown from 'react-markdown';
 3  import remarkGfm from 'remark-gfm';
 4  import rehypeHighlight from 'rehype-highlight';
 5  import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
 6  import './MarkdownContent.css';
 7  
 8  interface MarkdownContentProps {
 9    content: string;
10  }
11  
12  const CodeBlock = ({ children, className, ...props }: any) => {
13    const [copied, setCopied] = useState(false);
14    const codeRef = useRef<HTMLElement>(null);
15    
16    const match = /language-(\w+)/.exec(className || '');
17    const language = match ? match[1] : '';
18  
19    const handleCopy = () => {
20      if (codeRef.current) {
21        const text = codeRef.current.textContent || '';
22        navigator.clipboard.writeText(text).catch(() => {});
23        setCopied(true);
24        setTimeout(() => setCopied(false), 2000);
25      }
26    };
27  
28    return (
29      <div className="code-block-wrapper">
30        <div className="code-block-header">
31          <span className="code-block-lang">{language}</span>
32          <button className="code-block-copy" onClick={handleCopy} title="Copy code">
33            {copied ? <CheckOutlined /> : <CopyOutlined />}
34          </button>
35        </div>
36        <pre className="code-block-pre">
37          <code ref={codeRef} className={className} {...props}>
38            {children}
39          </code>
40        </pre>
41      </div>
42    );
43  };
44  
45  const MarkdownContent: React.FC<MarkdownContentProps> = React.memo(({ content }) => (
46    <div className="markdown-container" style={{ userSelect: 'text', cursor: 'text' }}>
47      <ReactMarkdown
48        remarkPlugins={[remarkGfm]}
49        rehypePlugins={[rehypeHighlight]}
50        components={{
51          pre: ({ children }) => <>{children}</>,
52          code: ({ children, className, node, ...props }: any) => {
53            const isBlock = /language-(\w+)/.exec(className || '') || className?.includes('hljs');
54            if (isBlock) {
55              return (
56                <CodeBlock className={className} {...props}>
57                  {children}
58                </CodeBlock>
59              );
60            }
61            return (
62              <code className="inline-code" {...props}>
63                {children}
64              </code>
65            );
66          },
67          table: ({ children }) => (
68            <div className="markdown-table-wrapper">
69              <table className="markdown-table">
70                {children}
71              </table>
72            </div>
73          ),
74          th: ({ children }) => <th>{children}</th>,
75          td: ({ children }) => <td>{children}</td>,
76          h1: ({ children }) => <h2 style={{ margin: '12px 0 4px' }}>{children}</h2>,
77          h2: ({ children }) => <h3 style={{ margin: '12px 0 4px' }}>{children}</h3>,
78          h3: ({ children }) => <h4 style={{ margin: '12px 0 4px' }}>{children}</h4>,
79          li: ({ children }) => <li style={{ marginLeft: 16 }}>{children}</li>,
80          p: ({ children }) => <p style={{ margin: '4px 0' }}>{children}</p>,
81        }}
82      >
83        {content}
84      </ReactMarkdown>
85    </div>
86  ));
87  
88  export default MarkdownContent;