diff --git a/frontend/package.json b/frontend/package.json index e31991f..1378c94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,8 +13,11 @@ "license": "ISC", "dependencies": { "@types/qrcode": "^1.5.6", + "highlight.js": "^11.11.1", "html2canvas": "^1.4.1", "jspdf": "^4.2.1", + "katex": "^0.17.0", + "mermaid": "^11.16.0", "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/pages/front/ChatPage.css b/frontend/src/pages/front/ChatPage.css index 0ce673f..fc48a08 100644 --- a/frontend/src/pages/front/ChatPage.css +++ b/frontend/src/pages/front/ChatPage.css @@ -507,6 +507,52 @@ background: rgba(255, 255, 255, 0.05); } +/* 数学公式 */ +.md-math-block { + display: block; + overflow-x: auto; + padding: 10px 0; + text-align: center; +} +.md-math-inline { + display: inline; + padding: 0 2px; +} + +/* 代码块包装器 + 复制按钮 */ +.md-code-wrapper { + position: relative; + margin: 12px 0; +} +.md-code-wrapper .md-code-block { + margin: 0; + border-radius: var(--radius-md); +} +.md-code-copy { + position: absolute; + top: 8px; + right: 8px; + padding: 4px 10px; + font-size: 12px; + color: #6B7280; + background: #F3F4F6; + border: 1px solid #E5E7EB; + border-radius: 4px; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + z-index: 2; +} +.md-code-wrapper:hover .md-code-copy { + opacity: 1; +} + +/* 删除线 */ +del { + text-decoration: line-through; + opacity: 0.6; +} + .md-hr { border: none; border-top: 1px solid var(--color-border); diff --git a/frontend/src/pages/front/ChatPage.tsx b/frontend/src/pages/front/ChatPage.tsx index 337998f..bf08806 100644 --- a/frontend/src/pages/front/ChatPage.tsx +++ b/frontend/src/pages/front/ChatPage.tsx @@ -4,6 +4,12 @@ import { chatAPI, knowledgeAPI, marketAPI, evolutionAPI } from '@/api'; import type { Conversation, Message } from '@/types'; import './ChatPage.css'; +// ===== 增强渲染依赖 ===== +import hljs from 'highlight.js'; +import 'highlight.js/styles/github.css'; +import katex from 'katex'; +import 'katex/dist/katex.min.css'; + // ===== Enhanced Markdown Renderer ===== // 媒体(图片/视频)以受限缩略图呈现,并提供下载(格式+尺寸)选项 interface MediaInfo { type: 'img' | 'video'; src: string; alt?: string; } @@ -209,6 +215,34 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => { } ); + // 2.8 LaTeX 数学公式:$$...$$(块级)和 $...$(行内) + // 提取为占位符,渲染后用 KaTeX 处理 + const mathBlocks: string[] = []; + text = text.replace(/\$\$([\s\S]+?)\$\$/g, (_m: string, formula: string) => { + try { + const rendered = katex.renderToString(formula.trim(), { displayMode: true, throwOnError: false }); + const idx = mathBlocks.length; + mathBlocks.push(`
${rendered}
`); + return '\x00MATHBLOCK' + idx + '\x00'; + } catch { return _m; } + }); + text = text.replace(/\$([^\s$][^$]*[^\s$])\$/g, (_m: string, formula: string) => { + try { + const rendered = katex.renderToString(formula.trim(), { displayMode: false, throwOnError: false }); + const idx = mathBlocks.length; + mathBlocks.push(`${rendered}`); + return '\x00MATHBLOCK' + idx + '\x00'; + } catch { return _m; } + }); + + // 2.9 Strikethrough: ~~text~~ → text + text = text.replace(/~~(.+?)~~/g, '$1'); + + // 2.10 Auto-link bare URLs: http(s)://... → + text = text.replace(/(?])https?:\/\/[^\s<>[\]()]+/g, (url: string) => + `${url}` + ); + // 3. Headers text = text.replace(/^#### (.+)$/gm, '

$1

'); text = text.replace(/^### (.+)$/gm, '

$1

'); @@ -243,6 +277,21 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => { text = text.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m: string, idx: string) => codeBlocks[parseInt(idx)] || ''); text = text.replace(/\x00INLINE(\d+)\x00/g, (_m: string, idx: string) => inlineCodes[parseInt(idx)] || ''); text = text.replace(/\x00TABLE(\d+)\x00/g, (_m: string, idx: string) => tableBlocks[parseInt(idx)] || ''); + text = text.replace(/\x00MATHBLOCK(\d+)\x00/g, (_m: string, idx: string) => mathBlocks[parseInt(idx)] || ''); + + // 10.5 Apply syntax highlighting to code blocks + add copy button + text = text.replace( + /
([\s\S]*?)<\/code><\/pre>/g,
+      (_m: string, code: string) => {
+        const decoded = code.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
+        let highlighted: string;
+        try { highlighted = hljs.highlightAuto(decoded).value; } catch { highlighted = code; }
+        return `
+ +
${highlighted}
+
`; + } + ); // 11. Wrap in paragraph and clean up text = '

' + text + '

';