feat(webui): render markdown in response bubbles

linuztx committed Jun 13, 2025 at 16:47 UTC be067adf965603bd50e27e5efbcd2491af320491
1 file changed +48 -7
webui/js/messages.js
+48 -7
@@ -1,5 +1,6 @@
1 // copy button
2 import { openImageModal } from "./image_modal.js";
3 +import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
4
5 function createCopyButton() {
6 const button = document.createElement("button");
@@ -12,11 +13,11 @@ function createCopyButton() {
13 let textToCopy;
14
15 if (container.classList.contains("kvps-row")) {
15 - textToCopy = container.querySelector(".kvps-val").textContent;
16 + textToCopy = container.querySelector(".kvps-val").innerText;
17 } else if (container.classList.contains("message-text")) {
17 - textToCopy = container.querySelector("span").textContent;
18 + textToCopy = container.querySelector("span").innerText;
19 } else {
19 - textToCopy = container.querySelector("span").textContent;
20 + textToCopy = container.querySelector("span").innerText;
21 }
22
23 try {
@@ -83,7 +84,8 @@ export function _drawMessage(
84 kvps = null,
85 messageClasses = [],
86 contentClasses = [],
86 - latex = false
87 + latex = false,
88 + markdown = false
89 ) {
90 const messageDiv = document.createElement("div");
91 messageDiv.classList.add("message", ...messageClasses);
@@ -97,6 +99,30 @@ export function _drawMessage(
99 drawKvps(messageDiv, kvps, latex);
100
101 if (content && content.trim().length > 0) {
102 + if (markdown) {
103 + const contentDiv = document.createElement("div");
104 + contentDiv.classList.add("msg-content", ...contentClasses);
105 +
106 + const spanElement = document.createElement("span"); // Wrapper span
107 + spanElement.innerHTML = marked.parse(content, { breaks: true });
108 + contentDiv.appendChild(spanElement);
109 +
110 + addCopyButtonToElement(contentDiv);
111 + messageDiv.appendChild(contentDiv);
112 +
113 + // KaTeX rendering for markdown
114 + if (window.renderMathInElement && latex) {
115 + renderMathInElement(contentDiv, {
116 + delimiters: [
117 + { left: '$$', right: '$$', display: true },
118 + { left: '$', right: '$', display: false },
119 + { left: '\\(', right: '\\)', display: false },
120 + { left: '\\[', right: '\\]', display: true }
121 + ],
122 + throwOnError: false,
123 + });
124 + }
125 + } else {
126 const preElement = document.createElement("pre");
127 preElement.classList.add("msg-content", ...contentClasses);
128 preElement.style.whiteSpace = "pre-wrap";
@@ -120,6 +146,7 @@ export function _drawMessage(
146 delimiters: [{ left: "$", right: "$", display: true }],
147 throwOnError: false,
148 });
149 + }
150 }
151 }
152
@@ -150,6 +177,7 @@ export function drawMessageDefault(
177 kvps,
178 ["message-ai", "message-default"],
179 ["msg-json"],
180 + false,
181 false
182 );
183 }
@@ -178,6 +206,7 @@ export function drawMessageAgent(
206 kvpsFlat,
207 ["message-ai", "message-agent"],
208 ["msg-json"],
209 + false,
210 false
211 );
212 }
@@ -200,6 +229,7 @@ export function drawMessageResponse(
229 null,
230 ["message-ai", "message-agent-response"],
231 [],
232 + true,
233 true
234 );
235 }
@@ -216,13 +246,14 @@ export function drawMessageDelegation(
246 _drawMessage(
247 messageContainer,
248 heading,
219 - messageContent,
249 + content,
250 temp,
251 true,
252 kvps,
253 ["message-ai", "message-agent", "message-agent-delegation"],
254 [],
225 - true
255 + true,
256 + false
257 );
258 }
259
@@ -340,6 +371,7 @@ export function drawMessageTool(
371 kvps,
372 ["message-ai", "message-tool"],
373 ["msg-output"],
374 + false,
375 false
376 );
377 }
@@ -362,6 +394,7 @@ export function drawMessageCodeExe(
394 null,
395 ["message-ai", "message-code-exe"],
396 [],
397 + false,
398 false
399 );
400 }
@@ -384,6 +417,7 @@ export function drawMessageBrowser(
417 kvps,
418 ["message-ai", "message-browser"],
419 ["msg-json"],
420 + false,
421 false
422 );
423 }
@@ -407,6 +441,7 @@ export function drawMessageAgentPlain(
441 kvps,
442 [...classes],
443 [],
444 + false,
445 false
446 );
447 messageContainer.classList.add("center-container");
@@ -451,6 +486,7 @@ export function drawMessageUtil(
486 kvps,
487 ["message-util"],
488 ["msg-json"],
489 + false,
490 false
491 );
492 messageContainer.classList.add("center-container");
@@ -556,7 +592,12 @@ function drawKvps(container, kvps, latex) {
592
593 if (window.renderMathInElement && latex) {
594 renderMathInElement(span, {
559 - delimiters: [{ left: "$", right: "$", display: true }],
595 + delimiters: [
596 + { left: "$$", right: "$$", display: true },
597 + { left: "$", right: "$", display: false },
598 + { left: "\\(", right: "\\)", display: false },
599 + { left: "\\[", right: "\\]", display: true }
600 + ],
601 throwOnError: false,
602 });
603 }