Skip to content

Commit c431645

Browse files
committed
feat: 聊天支持Mermaid流程图渲染
1 parent d3dc26f commit c431645

File tree

1 file changed

+87
-30
lines changed

1 file changed

+87
-30
lines changed

src/chatPanel.ts

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ class WebviewOutputChannel implements vscode.OutputChannel {
2222

2323
append(value: string): void {
2424
this.webview.postMessage({ role: 'model', content: value });
25-
//getOutputChannel().append(value);
25+
getOutputChannel().append(value);
2626
}
2727

2828
appendLine(value: string): void {
2929
this.append(value + '\n');
30-
//getOutputChannel().appendLine(value);
30+
getOutputChannel().append(value + '\n');
3131
}
3232

3333
clear(): void {
@@ -196,6 +196,26 @@ export class ChatPanel {
196196
border: none;
197197
cursor: pointer;
198198
}
199+
#mermaid-toggle-container {
200+
position: absolute;
201+
top: 10px;
202+
right: 80px;
203+
}
204+
#mermaid-toggle {
205+
cursor: pointer;
206+
}
207+
.mermaid {
208+
margin: 10px 0;
209+
}
210+
.mermaid-raw {
211+
display: none;
212+
}
213+
.mermaid-rendered .mermaid-raw {
214+
display: block;
215+
}
216+
.mermaid-rendered .mermaid {
217+
display: none;
218+
}
199219
</style>
200220
</head>
201221
<body>
@@ -205,12 +225,16 @@ export class ChatPanel {
205225
<button id="send">Send</button>
206226
<button id="stop" style="display: none;">Stop</button>
207227
<button id="new-session" style="position: absolute; top: 10px; right: 10px;">New Session</button>
208-
228+
<div id="mermaid-toggle-container">
229+
<input type="checkbox" id="mermaid-toggle">
230+
<label for="mermaid-toggle">Show Mermaid Raw Code</label>
231+
</div>
209232
<input type="checkbox" id="web-search">
210233
<label for="web-search">联网搜索</label>
211234
</div>
212235
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
213236
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
237+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/mermaid.min.js"></script>
214238
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js"></script>
215239
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js"></script>
216240
<script>
@@ -221,6 +245,13 @@ export class ChatPanel {
221245
const stopButton = document.getElementById('stop');
222246
const newSessionButton = document.getElementById('new-session');
223247
const webSearchCheckbox = document.getElementById('web-search');
248+
const mermaidToggle = document.getElementById('mermaid-toggle');
249+
250+
// 初始化 Mermaid
251+
mermaid.initialize({
252+
startOnLoad: false,
253+
theme: 'dark'
254+
});
224255
225256
// 配置代码高亮
226257
hljs.configure({ ignoreUnescapedHTML: true });
@@ -292,44 +323,68 @@ export class ChatPanel {
292323
* 把 container.innerHTML 里所有的 $$…$$ 块,
293324
* 用 katex.renderToString 直接渲染成 HTML
294325
*/
295-
function fnRenderDisplayMath(webviewDiv)
296-
{
297-
// 1) 获取原始 HTML
326+
function fnRenderDisplayMath(webviewDiv) {
298327
const strRawHtml = webviewDiv.innerHTML;
299-
300-
// 2) 匹配所有 $$…$$(非贪婪)
301328
const rgxDisplayMath = /\$\$([\s\S]+?)\$\$/g;
302-
303-
// 3) 替换成 katex 渲染结果
304-
const strReplacedHtml = strRawHtml.replace(rgxDisplayMath
305-
,
306-
(strMatch, strInnerTex) =>
307-
{
308-
try
309-
{
310-
// trim 首尾空白,保持 display 模式
329+
const strReplacedHtml = strRawHtml.replace(rgxDisplayMath, (strMatch, strInnerTex) => {
330+
try {
311331
const strTex = strInnerTex.replace(/^\s+|\s+$/g, '');
312-
return katex.renderToString(strTex
313-
,
314-
{
332+
return katex.renderToString(strTex, {
315333
displayMode: true,
316334
throwOnError: false
317335
});
318-
}
319-
catch (err)
320-
{
336+
} catch (err) {
321337
console.error('KaTeX render error:', err);
322-
// 渲染失败就返回原始 $$…$$
323338
return strMatch;
324339
}
325340
});
326-
327-
// 4) 更新回 DOM
328341
webviewDiv.innerHTML = strReplacedHtml;
329342
}
330343
344+
// 渲染 Mermaid 图表
345+
async function renderMermaid(webviewDiv) {
346+
const codeBlocks = webviewDiv.querySelectorAll('pre code.language-mermaid');
347+
for (const codeBlock of codeBlocks) {
348+
const parentPre = codeBlock.closest('pre');
349+
const mermaidCode = codeBlock.textContent;
350+
try {
351+
const { svg } = await mermaid.render('mermaid-diagram-' + Date.now(), mermaidCode);
352+
const mermaidDiv = document.createElement('div');
353+
mermaidDiv.className = 'mermaid';
354+
mermaidDiv.innerHTML = svg;
355+
356+
const rawDiv = document.createElement('div');
357+
rawDiv.className = 'mermaid-raw';
358+
rawDiv.innerHTML = \`<pre><code class="language-mermaid">\${mermaidCode}</code></pre>\`;
359+
360+
const container = document.createElement('div');
361+
container.className = 'mermaid-container';
362+
container.appendChild(mermaidDiv);
363+
container.appendChild(rawDiv);
364+
365+
parentPre.replaceWith(container);
366+
} catch (err) {
367+
console.error('Mermaid render error:', err);
368+
const errorDiv = document.createElement('div');
369+
errorDiv.className = 'mermaid-error';
370+
errorDiv.textContent = 'Failed to render Mermaid diagram';
371+
parentPre.replaceWith(errorDiv);
372+
}
373+
}
374+
}
375+
376+
// 切换 Mermaid 显示模式
377+
function toggleMermaidDisplay() {
378+
const containers = document.querySelectorAll('.mermaid-container');
379+
if (mermaidToggle.checked) {
380+
containers.forEach(container => container.classList.add('mermaid-rendered'));
381+
} else {
382+
containers.forEach(container => container.classList.remove('mermaid-rendered'));
383+
}
384+
}
385+
331386
// 渲染消息内容
332-
function renderMessage(role, content, index) {
387+
async function renderMessage(role, content, index) {
333388
const lastChild = chat.lastElementChild;
334389
let targetDiv;
335390
@@ -352,6 +407,7 @@ export class ChatPanel {
352407
});
353408
354409
fnRenderDisplayMath(targetDiv);
410+
await renderMermaid(targetDiv);
355411
356412
renderMathInElement(targetDiv, {
357413
delimiters: [
@@ -373,11 +429,11 @@ export class ChatPanel {
373429
}
374430
375431
// 处理 Webview 消息
376-
window.addEventListener('message', (event) => {
432+
window.addEventListener('message', async (event) => {
377433
const data = event.data;
378434
379435
if (data.role && data.content) {
380-
renderMessage(data.role, data.content, data.index);
436+
await renderMessage(data.role, data.content, data.index);
381437
return;
382438
}
383439
@@ -411,6 +467,7 @@ export class ChatPanel {
411467
412468
// 初始化事件监听
413469
setupCopyButtonDelegation();
470+
mermaidToggle.addEventListener('change', toggleMermaidDisplay);
414471
415472
// 发送消息
416473
sendButton.addEventListener('click', () => {
@@ -503,7 +560,7 @@ export class ChatPanel {
503560

504561
try {
505562
const tools = message.webSearch ? [apiTools.searchTool] : null;
506-
const nomalSystemPromot = "用markdown输出。";
563+
const nomalSystemPromot = "用markdown输出。数学公式要用$$包裹,每条一行不要换行。流程图(Mermaid)里的每个字符串都要用引号包裹。";
507564
const systemPrompt = message.webSearch ? "每次回答问题前,一定要先上网搜索一下再回答。" + nomalSystemPromot : nomalSystemPromot;
508565
const response = await callDeepSeekApi(
509566
this.conversation.map(msg => ({ role: msg.role, content: msg.content })),

0 commit comments

Comments
 (0)