Просмотр исходного кода

✨ feat(markdown): replace Semi UI MarkdownRender with react-markdown for enhanced rendering

- Replace Semi UI's MarkdownRender with react-markdown library for better performance and features
- Add comprehensive markdown rendering support including:
  * Math formulas with KaTeX
  * Code syntax highlighting with rehype-highlight
  * Mermaid diagrams support
  * GitHub Flavored Markdown (tables, task lists, etc.)
  * HTML preview for code blocks
  * Media file support (audio/video)
- Create new MarkdownRenderer component with enhanced features:
  * Copy code button with hover effects
  * Code folding for long code blocks
  * Responsive design for mobile devices
- Add white text styling for user messages to improve readability on blue backgrounds
- Update all components using markdown rendering:
  * MessageContent.js - playground chat messages
  * About/index.js - about page content
  * Home/index.js - home page content
  * NoticeModal.js - system notice modal
  * OtherSetting.js - settings page
- Install new dependencies: react-markdown, remark-math, remark-breaks, remark-gfm,
  rehype-katex, rehype-highlight, katex, mermaid, use-debounce, clsx
- Add comprehensive CSS styles in markdown.css for better theming and user experience
- Remove unused imports and optimize component imports

Breaking changes: None - maintains backward compatibility with existing markdown content
Apple\Apple 9 месяцев назад
Родитель
Сommit
71df716787

+ 11 - 1
web/package.json

@@ -11,26 +11,36 @@
     "@visactor/vchart": "~1.8.8",
     "@visactor/vchart-semi-theme": "~1.8.8",
     "axios": "^0.27.2",
+    "clsx": "^2.1.1",
     "country-flag-icons": "^1.5.19",
     "dayjs": "^1.11.11",
     "history": "^5.3.0",
     "i18next": "^23.16.8",
     "i18next-browser-languagedetector": "^7.2.0",
+    "katex": "^0.16.22",
     "lucide-react": "^0.511.0",
     "marked": "^4.1.1",
+    "mermaid": "^11.6.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-dropzone": "^14.2.3",
     "react-fireworks": "^1.0.4",
     "react-i18next": "^13.0.0",
     "react-icons": "^5.5.0",
+    "react-markdown": "^10.1.0",
     "react-router-dom": "^6.3.0",
     "react-telegram-login": "^1.1.2",
     "react-toastify": "^9.0.8",
     "react-turnstile": "^1.0.5",
+    "rehype-highlight": "^7.0.2",
+    "rehype-katex": "^7.0.1",
+    "remark-breaks": "^4.0.0",
+    "remark-gfm": "^4.0.1",
+    "remark-math": "^6.0.0",
     "semantic-ui-offline": "^2.5.0",
     "semantic-ui-react": "^2.1.3",
-    "sse": "https://github.com/mpetazzoni/sse.js"
+    "sse": "https://github.com/mpetazzoni/sse.js",
+    "use-debounce": "^10.0.4"
   },
   "scripts": {
     "dev": "vite",

+ 491 - 0
web/src/components/common/MarkdownRenderer.js

@@ -0,0 +1,491 @@
+import ReactMarkdown from 'react-markdown';
+import 'katex/dist/katex.min.css';
+import 'highlight.js/styles/default.css';
+import './markdown.css';
+import RemarkMath from 'remark-math';
+import RemarkBreaks from 'remark-breaks';
+import RehypeKatex from 'rehype-katex';
+import RemarkGfm from 'remark-gfm';
+import RehypeHighlight from 'rehype-highlight';
+import { useRef, useState, useEffect, useMemo } from 'react';
+import mermaid from 'mermaid';
+import React from 'react';
+import { useDebouncedCallback } from 'use-debounce';
+import clsx from 'clsx';
+import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
+import { copy } from '../../helpers/utils';
+import { IconCopy } from '@douyinfe/semi-icons';
+import { useTranslation } from 'react-i18next';
+
+mermaid.initialize({
+  startOnLoad: false,
+  theme: 'default',
+  securityLevel: 'loose',
+});
+
+export function Mermaid(props) {
+  const ref = useRef(null);
+  const [hasError, setHasError] = useState(false);
+
+  useEffect(() => {
+    if (props.code && ref.current) {
+      mermaid
+        .run({
+          nodes: [ref.current],
+          suppressErrors: true,
+        })
+        .catch((e) => {
+          setHasError(true);
+          console.error('[Mermaid] ', e.message);
+        });
+    }
+  }, [props.code]);
+
+  function viewSvgInNewWindow() {
+    const svg = ref.current?.querySelector('svg');
+    if (!svg) return;
+    const text = new XMLSerializer().serializeToString(svg);
+    const blob = new Blob([text], { type: 'image/svg+xml' });
+    const url = URL.createObjectURL(blob);
+    window.open(url, '_blank');
+  }
+
+  if (hasError) {
+    return null;
+  }
+
+  return (
+    <div
+      className={clsx('mermaid-container')}
+      style={{
+        cursor: 'pointer',
+        overflow: 'auto',
+        padding: '12px',
+        border: '1px solid var(--semi-color-border)',
+        borderRadius: '8px',
+        backgroundColor: 'var(--semi-color-bg-1)',
+        margin: '12px 0',
+      }}
+      ref={ref}
+      onClick={() => viewSvgInNewWindow()}
+    >
+      {props.code}
+    </div>
+  );
+}
+
+export function PreCode(props) {
+  const ref = useRef(null);
+  const [mermaidCode, setMermaidCode] = useState('');
+  const [htmlCode, setHtmlCode] = useState('');
+  const { t } = useTranslation();
+
+  const renderArtifacts = useDebouncedCallback(() => {
+    if (!ref.current) return;
+    const mermaidDom = ref.current.querySelector('code.language-mermaid');
+    if (mermaidDom) {
+      setMermaidCode(mermaidDom.innerText);
+    }
+    const htmlDom = ref.current.querySelector('code.language-html');
+    const refText = ref.current.querySelector('code')?.innerText;
+    if (htmlDom) {
+      setHtmlCode(htmlDom.innerText);
+    } else if (
+      refText?.startsWith('<!DOCTYPE') ||
+      refText?.startsWith('<svg') ||
+      refText?.startsWith('<?xml')
+    ) {
+      setHtmlCode(refText);
+    }
+  }, 600);
+
+  // 处理代码块的换行
+  useEffect(() => {
+    if (ref.current) {
+      const codeElements = ref.current.querySelectorAll('code');
+      const wrapLanguages = [
+        '',
+        'md',
+        'markdown',
+        'text',
+        'txt',
+        'plaintext',
+        'tex',
+        'latex',
+      ];
+      codeElements.forEach((codeElement) => {
+        let languageClass = codeElement.className.match(/language-(\w+)/);
+        let name = languageClass ? languageClass[1] : '';
+        if (wrapLanguages.includes(name)) {
+          codeElement.style.whiteSpace = 'pre-wrap';
+        }
+      });
+      setTimeout(renderArtifacts, 1);
+    }
+  }, []);
+
+  return (
+    <>
+      <pre
+        ref={ref}
+        style={{
+          position: 'relative',
+          backgroundColor: 'var(--semi-color-fill-0)',
+          border: '1px solid var(--semi-color-border)',
+          borderRadius: '6px',
+          padding: '12px',
+          margin: '12px 0',
+          overflow: 'auto',
+          fontSize: '14px',
+          lineHeight: '1.4',
+        }}
+      >
+        <div
+          className="copy-code-button"
+          style={{
+            position: 'absolute',
+            top: '8px',
+            right: '8px',
+            display: 'flex',
+            gap: '4px',
+            zIndex: 10,
+            opacity: 0,
+            transition: 'opacity 0.2s ease',
+          }}
+        >
+          <Tooltip content={t('复制代码')}>
+            <Button
+              size="small"
+              theme="borderless"
+              icon={<IconCopy />}
+              onClick={(e) => {
+                e.preventDefault();
+                e.stopPropagation();
+                if (ref.current) {
+                  const code = ref.current.querySelector('code')?.innerText ?? '';
+                  copy(code).then((success) => {
+                    if (success) {
+                      Toast.success(t('代码已复制到剪贴板'));
+                    } else {
+                      Toast.error(t('复制失败,请手动复制'));
+                    }
+                  });
+                }
+              }}
+              style={{
+                padding: '4px',
+                backgroundColor: 'var(--semi-color-bg-2)',
+                borderRadius: '4px',
+                cursor: 'pointer',
+                border: '1px solid var(--semi-color-border)',
+                boxShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',
+              }}
+            />
+          </Tooltip>
+        </div>
+        {props.children}
+      </pre>
+      {mermaidCode.length > 0 && (
+        <Mermaid code={mermaidCode} key={mermaidCode} />
+      )}
+      {htmlCode.length > 0 && (
+        <div
+          style={{
+            border: '1px solid var(--semi-color-border)',
+            borderRadius: '8px',
+            padding: '16px',
+            margin: '12px 0',
+            backgroundColor: 'var(--semi-color-bg-1)',
+          }}
+        >
+          <div style={{ marginBottom: '8px', fontSize: '12px', color: 'var(--semi-color-text-2)' }}>
+            HTML预览:
+          </div>
+          <div dangerouslySetInnerHTML={{ __html: htmlCode }} />
+        </div>
+      )}
+    </>
+  );
+}
+
+function CustomCode(props) {
+  const ref = useRef(null);
+  const [collapsed, setCollapsed] = useState(true);
+  const [showToggle, setShowToggle] = useState(false);
+  const { t } = useTranslation();
+
+  useEffect(() => {
+    if (ref.current) {
+      const codeHeight = ref.current.scrollHeight;
+      setShowToggle(codeHeight > 400);
+      ref.current.scrollTop = ref.current.scrollHeight;
+    }
+  }, [props.children]);
+
+  const toggleCollapsed = () => {
+    setCollapsed((collapsed) => !collapsed);
+  };
+
+  const renderShowMoreButton = () => {
+    if (showToggle && collapsed) {
+      return (
+        <div
+          style={{
+            position: 'absolute',
+            bottom: '8px',
+            right: '8px',
+            left: '8px',
+            display: 'flex',
+            justifyContent: 'center',
+          }}
+        >
+          <Button size="small" onClick={toggleCollapsed} theme="solid">
+            {t('显示更多')}
+          </Button>
+        </div>
+      );
+    }
+    return null;
+  };
+
+  return (
+    <div style={{ position: 'relative' }}>
+      <code
+        className={clsx(props?.className)}
+        ref={ref}
+        style={{
+          maxHeight: collapsed ? '400px' : 'none',
+          overflowY: 'hidden',
+          display: 'block',
+          padding: '8px 12px',
+          backgroundColor: 'var(--semi-color-fill-0)',
+          borderRadius: '4px',
+          fontSize: '13px',
+          lineHeight: '1.4',
+        }}
+      >
+        {props.children}
+      </code>
+      {renderShowMoreButton()}
+    </div>
+  );
+}
+
+function escapeBrackets(text) {
+  const pattern =
+    /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
+  return text.replace(
+    pattern,
+    (match, codeBlock, squareBracket, roundBracket) => {
+      if (codeBlock) {
+        return codeBlock;
+      } else if (squareBracket) {
+        return `$$${squareBracket}$$`;
+      } else if (roundBracket) {
+        return `$${roundBracket}$`;
+      }
+      return match;
+    },
+  );
+}
+
+function tryWrapHtmlCode(text) {
+  // 尝试包装HTML代码
+  if (text.includes('```')) {
+    return text;
+  }
+  return text
+    .replace(
+      /([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
+      (match, quoteStart, lang, newLine, doctype) => {
+        return !quoteStart ? '\n```html\n' + doctype : match;
+      },
+    )
+    .replace(
+      /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
+      (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
+        return !quoteEnd ? bodyEnd + space + htmlEnd + '\n```\n' : match;
+      },
+    );
+}
+
+function _MarkdownContent(props) {
+  const escapedContent = useMemo(() => {
+    return tryWrapHtmlCode(escapeBrackets(props.content));
+  }, [props.content]);
+
+  // 判断是否为用户消息
+  const isUserMessage = props.className && props.className.includes('user-message');
+
+  return (
+    <ReactMarkdown
+      remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
+      rehypePlugins={[
+        RehypeKatex,
+        [
+          RehypeHighlight,
+          {
+            detect: false,
+            ignoreMissing: true,
+          },
+        ],
+      ]}
+      components={{
+        pre: PreCode,
+        code: CustomCode,
+        p: (pProps) => <p {...pProps} dir="auto" style={{ lineHeight: '1.6', color: isUserMessage ? 'white' : 'inherit' }} />,
+        a: (aProps) => {
+          const href = aProps.href || '';
+          if (/\.(aac|mp3|opus|wav)$/.test(href)) {
+            return (
+              <figure style={{ margin: '12px 0' }}>
+                <audio controls src={href} style={{ width: '100%' }}></audio>
+              </figure>
+            );
+          }
+          if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
+            return (
+              <video controls style={{ width: '100%', maxWidth: '100%', margin: '12px 0' }}>
+                <source src={href} />
+              </video>
+            );
+          }
+          const isInternal = /^\/#/i.test(href);
+          const target = isInternal ? '_self' : aProps.target ?? '_blank';
+          return (
+            <a
+              {...aProps}
+              target={target}
+              style={{
+                color: isUserMessage ? '#87CEEB' : 'var(--semi-color-primary)',
+                textDecoration: 'none',
+              }}
+              onMouseEnter={(e) => {
+                e.target.style.textDecoration = 'underline';
+              }}
+              onMouseLeave={(e) => {
+                e.target.style.textDecoration = 'none';
+              }}
+            />
+          );
+        },
+        h1: (props) => <h1 {...props} style={{ fontSize: '24px', fontWeight: 'bold', margin: '20px 0 12px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
+        h2: (props) => <h2 {...props} style={{ fontSize: '20px', fontWeight: 'bold', margin: '18px 0 10px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
+        h3: (props) => <h3 {...props} style={{ fontSize: '18px', fontWeight: 'bold', margin: '16px 0 8px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
+        h4: (props) => <h4 {...props} style={{ fontSize: '16px', fontWeight: 'bold', margin: '14px 0 6px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
+        h5: (props) => <h5 {...props} style={{ fontSize: '14px', fontWeight: 'bold', margin: '12px 0 4px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
+        h6: (props) => <h6 {...props} style={{ fontSize: '13px', fontWeight: 'bold', margin: '10px 0 4px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
+        blockquote: (props) => (
+          <blockquote
+            {...props}
+            style={{
+              borderLeft: isUserMessage ? '4px solid rgba(255, 255, 255, 0.5)' : '4px solid var(--semi-color-primary)',
+              paddingLeft: '16px',
+              margin: '12px 0',
+              backgroundColor: isUserMessage ? 'rgba(255, 255, 255, 0.1)' : 'var(--semi-color-fill-0)',
+              padding: '8px 16px',
+              borderRadius: '0 4px 4px 0',
+              fontStyle: 'italic',
+              color: isUserMessage ? 'white' : 'inherit',
+            }}
+          />
+        ),
+        ul: (props) => <ul {...props} style={{ margin: '8px 0', paddingLeft: '20px', color: isUserMessage ? 'white' : 'inherit' }} />,
+        ol: (props) => <ol {...props} style={{ margin: '8px 0', paddingLeft: '20px', color: isUserMessage ? 'white' : 'inherit' }} />,
+        li: (props) => <li {...props} style={{ margin: '4px 0', lineHeight: '1.6', color: isUserMessage ? 'white' : 'inherit' }} />,
+        table: (props) => (
+          <div style={{ overflow: 'auto', margin: '12px 0' }}>
+            <table
+              {...props}
+              style={{
+                width: '100%',
+                borderCollapse: 'collapse',
+                border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
+                borderRadius: '6px',
+                overflow: 'hidden',
+              }}
+            />
+          </div>
+        ),
+        th: (props) => (
+          <th
+            {...props}
+            style={{
+              padding: '8px 12px',
+              backgroundColor: isUserMessage ? 'rgba(255, 255, 255, 0.2)' : 'var(--semi-color-fill-1)',
+              border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
+              fontWeight: 'bold',
+              textAlign: 'left',
+              color: isUserMessage ? 'white' : 'inherit',
+            }}
+          />
+        ),
+        td: (props) => (
+          <td
+            {...props}
+            style={{
+              padding: '8px 12px',
+              border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
+              color: isUserMessage ? 'white' : 'inherit',
+            }}
+          />
+        ),
+      }}
+    >
+      {escapedContent}
+    </ReactMarkdown>
+  );
+}
+
+export const MarkdownContent = React.memo(_MarkdownContent);
+
+export function MarkdownRenderer(props) {
+  const {
+    content,
+    loading,
+    fontSize = 14,
+    fontFamily = 'inherit',
+    className,
+    style,
+    ...otherProps
+  } = props;
+
+  return (
+    <div
+      className={clsx('markdown-body', className)}
+      style={{
+        fontSize: `${fontSize}px`,
+        fontFamily: fontFamily,
+        lineHeight: '1.6',
+        color: 'var(--semi-color-text-0)',
+        ...style,
+      }}
+      dir="auto"
+      {...otherProps}
+    >
+      {loading ? (
+        <div style={{
+          display: 'flex',
+          alignItems: 'center',
+          gap: '8px',
+          padding: '16px',
+          color: 'var(--semi-color-text-2)',
+        }}>
+          <div style={{
+            width: '16px',
+            height: '16px',
+            border: '2px solid var(--semi-color-border)',
+            borderTop: '2px solid var(--semi-color-primary)',
+            borderRadius: '50%',
+            animation: 'spin 1s linear infinite',
+          }} />
+          正在渲染...
+        </div>
+      ) : (
+        <MarkdownContent content={content} className={className} />
+      )}
+    </div>
+  );
+}
+
+export default MarkdownRenderer; 

+ 422 - 0
web/src/components/common/markdown.css

@@ -0,0 +1,422 @@
+/* 基础markdown样式 */
+.markdown-body {
+  font-family: inherit;
+  line-height: 1.6;
+  color: var(--semi-color-text-0);
+  overflow-wrap: break-word;
+  word-wrap: break-word;
+  word-break: break-word;
+}
+
+/* 用户消息样式 - 白色字体适配蓝色背景 */
+.user-message {
+  color: white !important;
+}
+
+.user-message .markdown-body {
+  color: white !important;
+}
+
+.user-message h1,
+.user-message h2,
+.user-message h3,
+.user-message h4,
+.user-message h5,
+.user-message h6 {
+  color: white !important;
+}
+
+.user-message p {
+  color: white !important;
+}
+
+.user-message span {
+  color: white !important;
+}
+
+.user-message div {
+  color: white !important;
+}
+
+.user-message li {
+  color: white !important;
+}
+
+.user-message td,
+.user-message th {
+  color: white !important;
+}
+
+.user-message blockquote {
+  color: white !important;
+  border-left-color: rgba(255, 255, 255, 0.5) !important;
+  background-color: rgba(255, 255, 255, 0.1) !important;
+}
+
+.user-message code:not(pre code) {
+  color: #000 !important;
+  background-color: rgba(255, 255, 255, 0.9) !important;
+}
+
+.user-message a {
+  color: #87CEEB !important;
+  /* 浅蓝色链接 */
+}
+
+.user-message a:hover {
+  color: #B0E0E6 !important;
+  /* hover时更浅的蓝色 */
+}
+
+/* 表格在用户消息中的样式 */
+.user-message table {
+  border-color: rgba(255, 255, 255, 0.3) !important;
+}
+
+.user-message th {
+  background-color: rgba(255, 255, 255, 0.2) !important;
+  border-color: rgba(255, 255, 255, 0.3) !important;
+}
+
+.user-message td {
+  border-color: rgba(255, 255, 255, 0.3) !important;
+}
+
+/* 加载动画 */
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+/* 代码高亮主题 - 适配Semi Design */
+.hljs {
+  display: block;
+  overflow-x: auto;
+  padding: 0;
+  background: transparent;
+  color: var(--semi-color-text-0);
+}
+
+.hljs-comment,
+.hljs-quote {
+  color: var(--semi-color-text-2);
+  font-style: italic;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-subst {
+  color: var(--semi-color-primary);
+  font-weight: bold;
+}
+
+.hljs-number,
+.hljs-literal,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-tag .hljs-attr {
+  color: var(--semi-color-warning);
+}
+
+.hljs-string,
+.hljs-doctag {
+  color: var(--semi-color-success);
+}
+
+.hljs-title,
+.hljs-section,
+.hljs-selector-id {
+  color: var(--semi-color-primary);
+  font-weight: bold;
+}
+
+.hljs-subst {
+  font-weight: normal;
+}
+
+.hljs-type,
+.hljs-class .hljs-title {
+  color: var(--semi-color-info);
+  font-weight: bold;
+}
+
+.hljs-tag,
+.hljs-name,
+.hljs-attribute {
+  color: var(--semi-color-primary);
+  font-weight: normal;
+}
+
+.hljs-regexp,
+.hljs-link {
+  color: var(--semi-color-tertiary);
+}
+
+.hljs-symbol,
+.hljs-bullet {
+  color: var(--semi-color-warning);
+}
+
+.hljs-built_in,
+.hljs-builtin-name {
+  color: var(--semi-color-info);
+}
+
+.hljs-meta {
+  color: var(--semi-color-text-2);
+}
+
+.hljs-deletion {
+  background: var(--semi-color-danger-light-default);
+}
+
+.hljs-addition {
+  background: var(--semi-color-success-light-default);
+}
+
+.hljs-emphasis {
+  font-style: italic;
+}
+
+.hljs-strong {
+  font-weight: bold;
+}
+
+/* Mermaid容器样式 */
+.mermaid-container {
+  transition: all 0.2s ease;
+}
+
+.mermaid-container:hover {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  transform: translateY(-1px);
+}
+
+/* 代码块样式增强 */
+pre {
+  position: relative;
+  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+  transition: all 0.2s ease;
+}
+
+pre:hover {
+  border-color: var(--semi-color-primary) !important;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+pre:hover .copy-code-button {
+  opacity: 1 !important;
+}
+
+.copy-code-button {
+  opacity: 0;
+  transition: opacity 0.2s ease;
+  z-index: 10;
+  pointer-events: auto;
+}
+
+.copy-code-button:hover {
+  opacity: 1 !important;
+}
+
+.copy-code-button button {
+  pointer-events: auto !important;
+  cursor: pointer !important;
+}
+
+/* 确保按钮可点击 */
+.copy-code-button .semi-button {
+  pointer-events: auto !important;
+  cursor: pointer !important;
+  transition: all 0.2s ease;
+}
+
+.copy-code-button .semi-button:hover {
+  background-color: var(--semi-color-fill-1) !important;
+  border-color: var(--semi-color-primary) !important;
+  transform: scale(1.05);
+}
+
+/* 表格响应式 */
+@media (max-width: 768px) {
+  .markdown-body table {
+    font-size: 12px;
+  }
+
+  .markdown-body th,
+  .markdown-body td {
+    padding: 6px 8px;
+  }
+}
+
+/* 数学公式样式 */
+.katex {
+  font-size: 1em;
+}
+
+.katex-display {
+  margin: 1em 0;
+  text-align: center;
+}
+
+/* 链接hover效果 */
+.markdown-body a {
+  transition: all 0.2s ease;
+}
+
+/* 引用块样式增强 */
+.markdown-body blockquote {
+  position: relative;
+}
+
+.markdown-body blockquote::before {
+  content: '"';
+  position: absolute;
+  left: -8px;
+  top: -8px;
+  font-size: 24px;
+  color: var(--semi-color-primary);
+  opacity: 0.3;
+}
+
+/* 列表样式增强 */
+.markdown-body ul li::marker {
+  color: var(--semi-color-primary);
+}
+
+.markdown-body ol li::marker {
+  color: var(--semi-color-primary);
+  font-weight: bold;
+}
+
+/* 分隔线样式 */
+.markdown-body hr {
+  border: none;
+  height: 1px;
+  background: linear-gradient(to right, transparent, var(--semi-color-border), transparent);
+  margin: 24px 0;
+}
+
+/* 图片样式 */
+.markdown-body img {
+  max-width: 100%;
+  height: auto;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  margin: 12px 0;
+}
+
+/* 内联代码样式 */
+.markdown-body code:not(pre code) {
+  background-color: var(--semi-color-fill-1);
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-size: 0.9em;
+  color: var(--semi-color-primary);
+  border: 1px solid var(--semi-color-border);
+}
+
+/* 标题锚点样式 */
+.markdown-body h1:hover,
+.markdown-body h2:hover,
+.markdown-body h3:hover,
+.markdown-body h4:hover,
+.markdown-body h5:hover,
+.markdown-body h6:hover {
+  position: relative;
+}
+
+/* 任务列表样式 */
+.markdown-body input[type="checkbox"] {
+  margin-right: 8px;
+  transform: scale(1.1);
+}
+
+.markdown-body li.task-list-item {
+  list-style: none;
+  margin-left: -20px;
+}
+
+/* 键盘按键样式 */
+.markdown-body kbd {
+  background-color: var(--semi-color-fill-0);
+  border: 1px solid var(--semi-color-border);
+  border-radius: 3px;
+  box-shadow: 0 1px 0 var(--semi-color-border);
+  color: var(--semi-color-text-0);
+  display: inline-block;
+  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+  font-size: 0.85em;
+  font-weight: 700;
+  line-height: 1;
+  padding: 2px 4px;
+  white-space: nowrap;
+}
+
+/* 详情折叠样式 */
+.markdown-body details {
+  border: 1px solid var(--semi-color-border);
+  border-radius: 6px;
+  padding: 12px;
+  margin: 12px 0;
+}
+
+.markdown-body summary {
+  cursor: pointer;
+  font-weight: bold;
+  color: var(--semi-color-primary);
+  margin-bottom: 8px;
+}
+
+.markdown-body summary:hover {
+  color: var(--semi-color-primary-hover);
+}
+
+/* 脚注样式 */
+.markdown-body .footnote-ref {
+  color: var(--semi-color-primary);
+  text-decoration: none;
+  font-weight: bold;
+}
+
+.markdown-body .footnote-ref:hover {
+  text-decoration: underline;
+}
+
+/* 警告块样式 */
+.markdown-body .warning {
+  background-color: var(--semi-color-warning-light-default);
+  border-left: 4px solid var(--semi-color-warning);
+  padding: 12px 16px;
+  margin: 12px 0;
+  border-radius: 0 6px 6px 0;
+}
+
+.markdown-body .info {
+  background-color: var(--semi-color-info-light-default);
+  border-left: 4px solid var(--semi-color-info);
+  padding: 12px 16px;
+  margin: 12px 0;
+  border-radius: 0 6px 6px 0;
+}
+
+.markdown-body .success {
+  background-color: var(--semi-color-success-light-default);
+  border-left: 4px solid var(--semi-color-success);
+  padding: 12px 16px;
+  margin: 12px 0;
+  border-radius: 0 6px 6px 0;
+}
+
+.markdown-body .danger {
+  background-color: var(--semi-color-danger-light-default);
+  border-left: 4px solid var(--semi-color-danger);
+  padding: 12px 16px;
+  margin: 12px 0;
+  border-radius: 0 6px 6px 0;
+}

+ 20 - 9
web/src/components/playground/MessageContent.js

@@ -1,11 +1,10 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React from 'react';
 import {
   Typography,
-  MarkdownRender,
   TextArea,
   Button,
-  Space,
 } from '@douyinfe/semi-ui';
+import MarkdownRenderer from '../common/MarkdownRenderer';
 import {
   ChevronRight,
   ChevronUp,
@@ -218,7 +217,10 @@ const MessageContent = ({
               <div className="p-3 sm:p-5 pt-2 sm:pt-4">
                 <div className="bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto max-h-50 overflow-y-auto">
                   <div className="prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm">
-                    <MarkdownRender raw={finalExtractedThinkingContent} />
+                    <MarkdownRenderer
+                      content={finalExtractedThinkingContent}
+                      className=""
+                    />
                   </div>
                 </div>
               </div>
@@ -304,8 +306,11 @@ const MessageContent = ({
 
                 {/* 显示文本内容 */}
                 {textContent && textContent.text && typeof textContent.text === 'string' && textContent.text.trim() !== '' && (
-                  <div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
-                    <MarkdownRender raw={textContent.text} />
+                  <div className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}>
+                    <MarkdownRenderer
+                      content={textContent.text}
+                      className={message.role === 'user' ? 'user-message' : ''}
+                    />
                   </div>
                 )}
               </div>
@@ -317,14 +322,20 @@ const MessageContent = ({
               if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
                 return (
                   <div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
-                    <MarkdownRender raw={finalDisplayableFinalContent} />
+                    <MarkdownRenderer
+                      content={finalDisplayableFinalContent}
+                      className=""
+                    />
                   </div>
                 );
               }
             } else {
               return (
-                <div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
-                  <MarkdownRender raw={message.content} />
+                <div className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}>
+                  <MarkdownRenderer
+                    content={message.content}
+                    className={message.role === 'user' ? 'user-message' : ''}
+                  />
                 </div>
               );
             }