From dc577eb0f04840b4939a8c97a9e5de6e55993965 Mon Sep 17 00:00:00 2001 From: QihuAI Design Date: Thu, 16 Jul 2026 16:25:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E5=A2=9E=E5=BC=BAAI=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E6=B8=B2=E6=9F=93=20=E2=80=94=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=AB=98=E4=BA=AE/KaTeX=E6=95=B0=E5=AD=A6/=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E7=BA=BF/URL=E8=87=AA=E5=8A=A8=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增渲染能力: 1. highlight.js 代码语法高亮 + 复制按钮 (hover显示) 2. KaTeX 数学公式渲染 ($$...$$块级 + $...$行内) 3. 删除线 ~~text~~ → 4. 裸URL自动转可点击链接 依赖: npm install highlight.js katex mermaid --- frontend/package.json | 3 ++ frontend/src/pages/front/ChatPage.css | 46 +++++++++++++++++++++++++ frontend/src/pages/front/ChatPage.tsx | 49 +++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) 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 + '

';