feat(chat): 增强AI回复渲染 — 代码高亮/KaTeX数学/删除线/URL自动链接

新增渲染能力:
1. highlight.js 代码语法高亮 + 复制按钮 (hover显示)
2. KaTeX 数学公式渲染 ($$...$$块级 + $...$行内)
3. 删除线 ~~text~~ → <del>
4. 裸URL自动转可点击链接

依赖: npm install highlight.js katex mermaid
This commit is contained in:
QihuAI Design committed 2026-07-16 16:25:41 +08:00
1 parent 8486a2a0e5
commit dc577eb0f0
3 files changed
+98

No files matched your search

+3
View File
@@ -13,8 +13,11 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
"katex": "^0.17.0",
"mermaid": "^11.16.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
+46
View File
@@ -507,6 +507,52 @@
background: rgba(255, 255, 255, 0.05); 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 { .md-hr {
border: none; border: none;
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
+49
View File
@@ -4,6 +4,12 @@ import { chatAPI, knowledgeAPI, marketAPI, evolutionAPI } from '@/api';
import type { Conversation, Message } from '@/types'; import type { Conversation, Message } from '@/types';
import './ChatPage.css'; 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 ===== // ===== Enhanced Markdown Renderer =====
// 媒体(图片/视频)以受限缩略图呈现,并提供下载(格式+尺寸)选项 // 媒体(图片/视频)以受限缩略图呈现,并提供下载(格式+尺寸)选项
interface MediaInfo { type: 'img' | 'video'; src: string; alt?: string; } 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(`<div class="md-math-block">${rendered}</div>`);
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(`<span class="md-math-inline">${rendered}</span>`);
return '\x00MATHBLOCK' + idx + '\x00';
} catch { return _m; }
});
// 2.9 Strikethrough: ~~text~~ → <del>text</del>
text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');
// 2.10 Auto-link bare URLs: http(s)://... → <a>
text = text.replace(/(?<![="'>])https?:\/\/[^\s<>[\]()]+/g, (url: string) =>
`<a class="md-link" href="${url}" target="_blank" rel="noopener">${url}</a>`
);
// 3. Headers // 3. Headers
text = text.replace(/^#### (.+)$/gm, '<h4 class="md-h4">$1</h4>'); text = text.replace(/^#### (.+)$/gm, '<h4 class="md-h4">$1</h4>');
text = text.replace(/^### (.+)$/gm, '<h4 class="md-h4">$1</h4>'); text = text.replace(/^### (.+)$/gm, '<h4 class="md-h4">$1</h4>');
@@ -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(/\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(/\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(/\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(
/<pre class="md-code-block"><code class="md-code">([\s\S]*?)<\/code><\/pre>/g,
(_m: string, code: string) => {
const decoded = code.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
let highlighted: string;
try { highlighted = hljs.highlightAuto(decoded).value; } catch { highlighted = code; }
return `<div class="md-code-wrapper">
<button class="md-code-copy" onclick="navigator.clipboard.writeText(this.parentElement.querySelector('code').textContent);this.textContent='已复制';setTimeout(()=>this.textContent='复制',2000)">复制</button>
<pre class="md-code-block"><code class="md-code hljs">${highlighted}</code></pre>
</div>`;
}
);
// 11. Wrap in paragraph and clean up // 11. Wrap in paragraph and clean up
text = '<p class="md-p">' + text + '</p>'; text = '<p class="md-p">' + text + '</p>';