Chandra实操手册:Chandra前端添加Markdown渲染、LaTeX公式、Mermaid流程图支持
1. 为什么需要在Chandra里支持富文本显示
你有没有遇到过这样的情况:和AI聊着聊着,它突然给你甩出一段带数学公式的解释,或者画了个流程图说明逻辑,又或者用代码块展示一个完整示例——结果你眼前只看到一串乱糟糟的原始文本?gemma:2b模型本身已经能生成结构清晰、格式丰富的回答,但默认的Chandra前端却像一张白纸,把所有精心排版的内容都“拍平”成纯文字。
这不是模型能力的问题,而是前端展示层的短板。真正的AI对话体验,不该止步于“能说话”,而要进阶到“会表达”:公式该清晰就清晰,流程图该直观就直观,代码该高亮就高亮,列表该对齐就对齐。这正是我们这次升级的核心目标——让Chandra不仅能说,还能漂亮地说。
整个过程不依赖外部服务,不修改Ollama内核,也不动后端API,纯粹从前端入手,轻量、安全、可复现。你不需要成为前端专家,只要跟着步骤操作,15分钟内就能让自己的Chandra聊天界面焕然一新。
2. 技术方案选型与设计原则
2.1 为什么选这三个库:简洁、可靠、零侵入
我们没有选择重型框架或全功能编辑器,而是聚焦三个轻量级、专注单一能力的开源库,它们共同构成了本次升级的“富文本三件套”:
marked:负责把Markdown语法(如**加粗**、> 引用、code)转成标准HTML。它体积小(仅约20KB)、无依赖、解析速度快,且完全运行在浏览器端,不向服务器发送任何内容。katex:专精于LaTeX数学公式的渲染。相比MathJax,它启动更快、样式更现代、对中文兼容更好,且默认支持行内公式$E=mc^2$和独立公式$$\int_0^\infty e^{-x^2}dx = \frac{\sqrt{\pi}}{2}$$。mermaid:业界事实标准的流程图/时序图/类图生成库。它用纯文本描述图形(如graph LR; A-->B; B-->C),再实时渲染为SVG矢量图,清晰锐利,缩放不失真。
关键设计原则
- 零后端改动:所有逻辑都在前端JavaScript中完成,Ollama和Chandra后端API保持原样,不新增接口、不改响应格式。
- 安全第一:所有渲染均在沙箱环境中执行,自动过滤
<script>、onerror等危险HTML标签,防止XSS攻击。- 渐进式增强:如果某条消息不含Markdown/LaTeX/Mermaid,它就按原样显示;只有检测到对应语法时,才触发相应渲染,绝不影响原有体验。
- 资源友好:三个库总大小控制在150KB以内,首次加载后缓存,后续对话毫秒级响应。
2.2 渲染时机与作用域:只处理AI回复,不碰用户输入
我们明确界定:富文本渲染只应用于AI模型返回的response内容,绝不处理用户输入框里的文字。原因很实在——用户输入是命令、是提问、是意图表达,它本就不该被“美化”;而AI的回复是信息载体、是知识输出、是解决方案,它值得被更好地呈现。
因此,整个流程是单向的:
用户输入 → 原样发给Ollama API → Ollama返回原始文本 → 前端JS识别其中的Markdown/LaTeX/Mermaid片段 → 调用对应库渲染 → 插入聊天窗口这个设计既保证了语义清晰,也避免了因误渲染用户输入而引发的意外行为(比如把用户写的$100当成公式渲染)。
3. 实战:三步完成前端增强
3.1 第一步:注入依赖库(修改index.html)
打开Chandra项目的根目录,找到public/index.html文件(若使用Docker镜像,需先进入容器或挂载修改)。在<head>标签内,添加以下CDN链接:
<!-- 在 <head> 中添加 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css"> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.0/dist/mermaid.min.js"></script>注意:这里使用的是稳定版本CDN,确保每次加载内容一致。
mermaid需在katex之后加载,因其内部依赖部分数学符号渲染能力。
3.2 第二步:编写渲染核心逻辑(创建render.js)
在public/目录下新建文件render.js,粘贴以下代码。这段脚本将接管所有AI回复的渲染任务:
// public/render.js (function () { // 初始化 Mermaid mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' }); // 定义渲染函数 window.renderMessage = function (rawText) { if (!rawText || typeof rawText !== 'string') return rawText; // 步骤1:用 marked 解析 Markdown let html = marked.parse(rawText, { gfm: true, breaks: true, highlight: function (code, lang) { // 简单代码高亮(可选,如需更专业请集成highlight.js) return `<pre><code class="language-${lang || ''}">${code}</code></pre>`; } }); // 步骤2:用 KaTeX 渲染 LaTeX 公式 html = html.replace(/\$\$(.*?)\$\$/gms, (match, p1) => { try { return katex.renderToString(p1.trim(), { displayMode: true }); } catch (e) { return `<span class="katex-error">[LaTeX error: ${e.message}]</span>`; } }).replace(/\$(.*?)\$/g, (match, p1) => { try { return katex.renderToString(p1.trim(), { displayMode: false }); } catch (e) { return `<span class="katex-error">[LaTeX error]</span>`; } }); // 步骤3:用 Mermaid 渲染流程图(匹配 ```mermaid ... ``` 块) const mermaidRegex = /```mermaid\s*([\s\S]*?)\s*```/g; let tempId = 0; html = html.replace(mermaidRegex, (match, p1) => { const id = `mermaid-${++tempId}`; return `<div class="mermaid-container"><div id="${id}"></div></div>`; }); // 返回处理后的HTML字符串 return html; }; // 暴露一个初始化函数,供聊天组件调用 window.initMermaid = function () { // 延迟执行,确保DOM已就位 setTimeout(() => { const containers = document.querySelectorAll('.mermaid-container'); containers.forEach(container => { const div = container.querySelector('div'); if (div && div.id) { try { mermaid.render(div.id, div.textContent.trim() || 'graph LR; A-->B;'); } catch (e) { console.warn('Mermaid render failed:', e); div.innerHTML = '<em>图表渲染失败,请检查语法</em>'; } } }); }, 100); }; })();然后,在index.html的<body>底部,添加对render.js的引用:
<!-- 在 </body> 之前添加 --> <script src="./render.js"></script>3.3 第三步:改造聊天组件(修改ChatMessage.vue或等效JS逻辑)
Chandra前端通常基于Vue或纯JS实现。以最常见的Vue单文件组件为例,找到负责渲染单条消息的组件(如src/components/ChatMessage.vue),定位到AI消息的模板区域。
假设原代码类似这样:
<!-- 原始:纯文本显示 --> <div v-if="message.role === 'assistant'" class="message-content"> {{ message.content }} </div>将其替换为:
<!-- 升级后:支持富文本 --> <div v-if="message.role === 'assistant'" class="message-content" v-html="renderedContent" @DOMNodeInserted="onNodeInserted"> </div>并在<script>部分添加对应的逻辑:
export default { props: ['message'], data() { return { renderedContent: '' } }, mounted() { this.updateRenderedContent(); }, watch: { 'message.content': 'updateRenderedContent' }, methods: { updateRenderedContent() { if (this.message.role === 'assistant' && this.message.content) { // 调用我们封装的渲染函数 this.renderedContent = window.renderMessage(this.message.content); // 触发Mermaid渲染 this.$nextTick(() => { if (window.initMermaid) window.initMermaid(); }); } else { this.renderedContent = this.message.content || ''; } }, onNodeInserted() { // Vue 3中可用此钩子监听DOM变化,触发mermaid重绘(兼容性兜底) if (window.initMermaid) window.initMermaid(); } } }关键点说明:
- 使用
v-html而非{{ }},这是渲染HTML的唯一方式;@DOMNodeInserted是Vue 2的旧钩子,Vue 3中建议改用MutationObserver或直接在mounted/updated中调用initMermaid;- 所有错误都做了降级处理(如LaTeX语法错显示提示文字,Mermaid失败显示警告),确保不影响整体功能。
4. 效果验证与典型用例演示
4.1 测试用例清单:三类内容一键验证
启动修改后的Chandra服务,进入聊天界面,依次发送以下三条测试消息,观察AI回复效果:
| 测试类型 | 用户输入示例 | AI应答预期效果 |
|---|---|---|
| Markdown | 请用列表总结AI模型的三个核心能力,并用代码块展示一个Python调用示例 | 应显示带序号的列表、加粗标题、以及语法高亮的<pre><code>块 |
| LaTeX | 请写出质能方程,并推导其在相对论中的意义 | 应在文本中嵌入清晰的$E=mc^2$行内公式,以及独立居中的推导公式块 |
| Mermaid | 请用流程图描述一次HTTP请求的完整生命周期 | 应渲染出SVG格式的横向流程图,节点圆角、箭头清晰、文字可读 |
全部通过即表示集成成功。
4.2 真实场景效果对比(文字描述)
- 未增强前:AI回复的LaTeX公式
$F = ma$直接显示为$F = ma$,用户需自行脑补;Mermaid代码块原样打印,毫无图形; - 增强后:
$F = ma$变成优雅的斜体数学字体,独立公式块居中渲染,字号适中;Mermaid代码被替换成交互式SVG图,鼠标悬停有提示,缩放不失真。
更重要的是,所有这些增强都发生在用户浏览器本地。你的gemma:2b模型依然安静地运行在Ollama容器里,数据从未离开你的机器,只是前端多了一双“慧眼”,读懂了AI想表达的丰富语义。
5. 进阶优化与维护建议
5.1 性能微调:懒加载与缓存策略
对于高频使用的用户,可进一步优化首屏加载速度:
- 将
marked、katex、mermaid的CDN地址替换为本地副本(放入public/libs/),避免CDN抖动; - 对
katex字体文件启用HTTP缓存头,减少重复下载; - 在
render.js中增加简单缓存机制,对相同rawText的渲染结果做内存缓存(适用于重复问答场景)。
5.2 安全加固:严格的内容过滤
虽然marked默认已禁用HTML,但为万全起见,可在renderMessage函数开头加入白名单过滤:
// 在 renderMessage 函数内,解析前加入 rawText = rawText.replace(/<(?!(\/?(h[1-6]|p|br|ul|ol|li|strong|em|code|pre|blockquote|a|img)\b))[^>]*>/gi, '');该正则仅允许Markdown生成的安全标签(如<p>、<code>),彻底阻断任意HTML注入可能。
5.3 未来可扩展方向
本次方案留有清晰的扩展接口:
- 支持
highlight.js替换当前简易高亮,增加50+语言支持; - 集成
chart.js,让AI能生成` ```chart {type: 'bar', data: [...]}``并渲染为交互图表; - 添加“复制渲染后内容”按钮,方便用户一键复制含格式的文本到其他平台。
这些都不是必须项,而是当你需要时,可以轻松插上的模块。
6. 总结:让私有AI真正“活”起来
我们从一个朴素的问题出发:既然gemma:2b能生成结构化内容,为什么前端不能理解它?答案不是推倒重来,而是在尊重原有架构的前提下,做一次精准的“前端赋能”。
这一次升级,没有动Ollama一行配置,没有改gemma一个参数,甚至没碰Chandra后端API。我们只是在浏览器里,悄悄装上了一副“智能眼镜”——它让Markdown有了层次,让LaTeX有了尊严,让Mermaid有了生命。
最终交付的,不是一个炫技的Demo,而是一个开箱即用、安全可靠、持续可用的生产力工具。它印证了一个事实:私有化AI的价值,不仅在于“数据不出门”,更在于“表达不打折”。当你的AI助手既能思考,又能清晰、美观、专业地表达所思,那才是真正属于你自己的智慧伙伴。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。