Merge pull request #1168 from nimitbhardwaj/fix/latex-rendering

fix: render LaTeX math in chat messages
This commit is contained in:
Jaber Jaber
2026-05-12 15:08:15 +03:00
committed by GitHub
2 changed files with 28 additions and 4 deletions
+5 -4
View File
@@ -90,11 +90,11 @@ pub async fn webchat_page() -> impl IntoResponse {
let html = WEBCHAT_HTML.replace(NONCE_PLACEHOLDER, &nonce);
let csp = format!(
"default-src 'self'; \
script-src 'self' 'nonce-{nonce}' 'unsafe-eval'; \
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com; \
script-src 'self' 'nonce-{nonce}' 'unsafe-eval' https://cdn.jsdelivr.net; \
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net; \
img-src 'self' data: blob:; \
connect-src 'self' ws://localhost:* ws://127.0.0.1:* wss://localhost:* wss://127.0.0.1:*; \
font-src 'self' https://fonts.gstatic.com; \
connect-src 'self' ws://localhost:* ws://127.0.0.1:* wss://localhost:* wss://127.0.0.1:* https://cdn.jsdelivr.net; \
font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; \
media-src 'self' blob:; \
frame-src 'self' blob:; \
object-src 'none'; \
@@ -120,6 +120,7 @@ pub async fn webchat_page() -> impl IntoResponse {
/// All vendor libraries (Alpine.js, marked.js, highlight.js) are bundled
/// locally — no CDN dependency. Alpine.js is included LAST because it
/// immediately processes x-data directives and fires alpine:init on load.
/// KaTeX is loaded dynamically from jsdelivr CDN when needed for LaTeX rendering.
const WEBCHAT_HTML: &str = concat!(
include_str!("../static/index_head.html"),
"<style>\n",
@@ -143,6 +143,29 @@ function chatPage() {
// Fetch dynamic commands from server
this.fetchCommands();
// Observe DOM for new messages and render LaTeX
this._latexObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
var bubbles = node.querySelector ? node.querySelectorAll('.message-bubble') : [];
if (node.classList && node.classList.contains('message-bubble')) {
bubbles = [node];
}
bubbles.forEach(function(bubble) {
if (bubble.textContent && hasLatexDelimiters(bubble.textContent)) {
renderLatex(bubble);
}
});
}
});
});
});
this._latexObserver.observe(document.getElementById('messages') || document.body, {
childList: true,
subtree: true
});
// Ctrl+/ keyboard shortcut
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === '/') {