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:
1 parent
8486a2a0e5
commit
dc577eb0f0
3 files changed
+98
No files matched your search
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(/</g, '<').replace(/>/g, '>').replace(/&/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>';
|
||||||
|
|||||||
Reference in new issue
Block a user