Skip to content

Commit ffbe8f3

Browse files
committed
Render LaTeX math in chat too via shared lib/mathrender.js
1 parent 43de620 commit ffbe8f3

3 files changed

Lines changed: 98 additions & 83 deletions

File tree

chat.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,14 @@
403403
max-width: 100%;
404404
display: block;
405405
}
406+
.message-text .math {
407+
font-family: 'Cambria Math', 'STIX Two Math', 'Latin Modern Math', Georgia, serif;
408+
white-space: nowrap;
409+
}
410+
.message-text sup, .message-text sub {
411+
font-size: 0.72em;
412+
line-height: 0;
413+
}
406414
.chat-input {
407415
padding: 16px 20px;
408416
border-top: 1px solid rgba(255, 255, 255, 0.08);
@@ -702,7 +710,8 @@
702710
</div>
703711
</div>
704712
</div>
705-
<script src="lib/markdown.js"></script>
713+
<script src="lib/markdown.js"></script>
714+
<script src="lib/mathrender.js"></script>
706715
<!-- PrismJS core and autoloader for language components -->
707716
<script src="./node_modules/prismjs/prism.min.js"></script>
708717
<script src="./node_modules/prismjs/plugins/autoloader/prism-autoloader.min.js"></script>
@@ -858,6 +867,7 @@
858867
// Format assistant messages as markdown
859868
if (type === 'assistant') {
860869
textDiv.innerHTML = formatMarkdown(text);
870+
if (window.renderMathInElement) window.renderMathInElement(textDiv);
861871
} else {
862872
textDiv.textContent = text;
863873
}

lib/mathrender.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
(function () {
2+
function escapeHtmlStr(text) {
3+
return String(text)
4+
.replace(/&/g, '&amp;')
5+
.replace(/</g, '&lt;')
6+
.replace(/>/g, '&gt;');
7+
}
8+
9+
function latexToHtml(tex) {
10+
let s = escapeHtmlStr(tex);
11+
s = s.replace(/\\frac\s*\{([^{}]*)\}\s*\{([^{}]*)\}/g, '($1)/($2)');
12+
s = s.replace(/\\(?:text|mathrm|mathbf|mathit|mathcal|operatorname)\s*\{([^{}]*)\}/g, '$1');
13+
const sym = {
14+
'\\cdot':'·','\\times':'×','\\div':'÷','\\pm':'±','\\mp':'∓',
15+
'\\le':'≤','\\leq':'≤','\\ge':'≥','\\geq':'≥','\\neq':'≠','\\ne':'≠',
16+
'\\to':'→','\\rightarrow':'→','\\leftarrow':'←','\\Rightarrow':'⇒','\\iff':'⇔',
17+
'\\infty':'∞','\\in':'∈','\\notin':'∉','\\subset':'⊂','\\subseteq':'⊆',
18+
'\\cup':'∪','\\cap':'∩','\\forall':'∀','\\exists':'∃','\\emptyset':'∅',
19+
'\\ldots':'…','\\dots':'…','\\cdots':'⋯','\\equiv':'≡','\\approx':'≈','\\sim':'∼',
20+
'\\sum':'∑','\\prod':'∏','\\int':'∫','\\sqrt':'√','\\partial':'∂','\\nabla':'∇',
21+
'\\alpha':'α','\\beta':'β','\\gamma':'γ','\\delta':'δ','\\epsilon':'ε','\\varepsilon':'ε',
22+
'\\zeta':'ζ','\\eta':'η','\\theta':'θ','\\kappa':'κ','\\lambda':'λ','\\mu':'μ',
23+
'\\nu':'ν','\\xi':'ξ','\\pi':'π','\\rho':'ρ','\\sigma':'σ','\\tau':'τ',
24+
'\\phi':'φ','\\varphi':'φ','\\chi':'χ','\\psi':'ψ','\\omega':'ω',
25+
'\\Gamma':'Γ','\\Delta':'Δ','\\Theta':'Θ','\\Lambda':'Λ','\\Pi':'Π','\\Sigma':'Σ','\\Phi':'Φ','\\Omega':'Ω',
26+
'\\bmod':'mod','\\mod':'mod','\\gcd':'gcd','\\log':'log','\\ln':'ln','\\lg':'lg',
27+
'\\min':'min','\\max':'max','\\deg':'deg','\\dim':'dim',
28+
'\\lfloor':'⌊','\\rfloor':'⌋','\\lceil':'⌈','\\rceil':'⌉','\\langle':'⟨','\\rangle':'⟩',
29+
'\\,':' ','\\;':' ','\\:':' ','\\!':'','\\quad':' ','\\qquad':' '
30+
};
31+
s = s.replace(/\\[a-zA-Z]+|\\[,;:!]/g, m => (sym[m] !== undefined ? sym[m] : ''));
32+
s = s.replace(/\\%/g, '%').replace(/\\_/g, '_').replace(/\\&/g, '&amp;').replace(/\\#/g, '#');
33+
s = s.replace(/\^\{([^{}]*)\}/g, '<sup>$1</sup>');
34+
s = s.replace(/\^([A-Za-z0-9])/g, '<sup>$1</sup>');
35+
s = s.replace(/_\{([^{}]*)\}/g, '<sub>$1</sub>');
36+
s = s.replace(/_([A-Za-z0-9])/g, '<sub>$1</sub>');
37+
s = s.replace(/[{}]/g, '');
38+
return s;
39+
}
40+
41+
function convertMathText(text) {
42+
if (text.indexOf('$') === -1) return null;
43+
const re = /\$\$([\s\S]+?)\$\$|\$([^$\n]+?)\$/g;
44+
let out = '', last = 0, m, found = false;
45+
while ((m = re.exec(text)) !== null) {
46+
found = true;
47+
out += escapeHtmlStr(text.slice(last, m.index));
48+
const tex = m[1] != null ? m[1] : m[2];
49+
out += '<span class="math">' + latexToHtml(tex) + '</span>';
50+
last = re.lastIndex;
51+
}
52+
if (!found) return null;
53+
out += escapeHtmlStr(text.slice(last));
54+
return out;
55+
}
56+
57+
function renderMathInElement(root) {
58+
if (!root || typeof document === 'undefined') return;
59+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
60+
acceptNode(node) {
61+
if (!node.nodeValue || node.nodeValue.indexOf('$') === -1) return NodeFilter.FILTER_REJECT;
62+
let p = node.parentNode;
63+
while (p && p !== root) {
64+
const tag = p.nodeName;
65+
if (tag === 'CODE' || tag === 'PRE' || tag === 'SCRIPT' || tag === 'STYLE') return NodeFilter.FILTER_REJECT;
66+
p = p.parentNode;
67+
}
68+
return NodeFilter.FILTER_ACCEPT;
69+
}
70+
});
71+
const targets = [];
72+
let n;
73+
while ((n = walker.nextNode())) targets.push(n);
74+
targets.forEach(node => {
75+
const html = convertMathText(node.nodeValue);
76+
if (html == null) return;
77+
const span = document.createElement('span');
78+
span.innerHTML = html;
79+
node.parentNode.replaceChild(span, node);
80+
});
81+
}
82+
83+
window.renderMathInElement = renderMathInElement;
84+
})();

llm-response.html

Lines changed: 3 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>OpenCluely</title>
77
<script src="lib/markdown.js"></script>
8+
<script src="lib/mathrender.js"></script>
89
<style>
910
.text-white { color: white; }
1011
.text-gray-300 { color: #d1d5db; }
@@ -834,7 +835,7 @@
834835
// Render text content
835836
const textHtml = renderMarkdown(textContent);
836837
document.getElementById('text-content').innerHTML = textHtml;
837-
renderMathInElement(document.getElementById('text-content'));
838+
if (window.renderMathInElement) window.renderMathInElement(document.getElementById('text-content'));
838839

839840
// Render code blocks
840841
const codeContainer = document.getElementById('code-content');
@@ -879,7 +880,7 @@
879880
const html = renderMarkdown(response);
880881
const full = document.getElementById('full-markdown');
881882
full.innerHTML = html;
882-
renderMathInElement(full);
883+
if (window.renderMathInElement) window.renderMathInElement(full);
883884

884885
// Highlight any code if Prism is available
885886
if (typeof Prism !== 'undefined') {
@@ -907,86 +908,6 @@
907908
return div.innerHTML;
908909
}
909910

910-
function escapeHtmlStr(text) {
911-
return String(text)
912-
.replace(/&/g, '&amp;')
913-
.replace(/</g, '&lt;')
914-
.replace(/>/g, '&gt;');
915-
}
916-
917-
function latexToHtml(tex) {
918-
let s = escapeHtmlStr(tex);
919-
s = s.replace(/\\frac\s*\{([^{}]*)\}\s*\{([^{}]*)\}/g, '($1)/($2)');
920-
s = s.replace(/\\(?:text|mathrm|mathbf|mathit|mathcal|operatorname)\s*\{([^{}]*)\}/g, '$1');
921-
const sym = {
922-
'\\cdot':'·','\\times':'×','\\div':'÷','\\pm':'±','\\mp':'∓',
923-
'\\le':'≤','\\leq':'≤','\\ge':'≥','\\geq':'≥','\\neq':'≠','\\ne':'≠',
924-
'\\to':'→','\\rightarrow':'→','\\leftarrow':'←','\\Rightarrow':'⇒','\\iff':'⇔',
925-
'\\infty':'∞','\\in':'∈','\\notin':'∉','\\subset':'⊂','\\subseteq':'⊆',
926-
'\\cup':'∪','\\cap':'∩','\\forall':'∀','\\exists':'∃','\\emptyset':'∅',
927-
'\\ldots':'…','\\dots':'…','\\cdots':'⋯','\\equiv':'≡','\\approx':'≈','\\sim':'∼',
928-
'\\sum':'∑','\\prod':'∏','\\int':'∫','\\sqrt':'√','\\partial':'∂','\\nabla':'∇',
929-
'\\alpha':'α','\\beta':'β','\\gamma':'γ','\\delta':'δ','\\epsilon':'ε','\\varepsilon':'ε',
930-
'\\zeta':'ζ','\\eta':'η','\\theta':'θ','\\kappa':'κ','\\lambda':'λ','\\mu':'μ',
931-
'\\nu':'ν','\\xi':'ξ','\\pi':'π','\\rho':'ρ','\\sigma':'σ','\\tau':'τ',
932-
'\\phi':'φ','\\varphi':'φ','\\chi':'χ','\\psi':'ψ','\\omega':'ω',
933-
'\\Gamma':'Γ','\\Delta':'Δ','\\Theta':'Θ','\\Lambda':'Λ','\\Pi':'Π','\\Sigma':'Σ','\\Phi':'Φ','\\Omega':'Ω',
934-
'\\bmod':'mod','\\mod':'mod','\\gcd':'gcd','\\log':'log','\\ln':'ln','\\lg':'lg',
935-
'\\min':'min','\\max':'max','\\deg':'deg','\\dim':'dim',
936-
'\\lfloor':'⌊','\\rfloor':'⌋','\\lceil':'⌈','\\rceil':'⌉','\\langle':'⟨','\\rangle':'⟩',
937-
'\\,':' ','\\;':' ','\\:':' ','\\!':'','\\quad':' ','\\qquad':' '
938-
};
939-
s = s.replace(/\\[a-zA-Z]+|\\[,;:!]/g, m => (sym[m] !== undefined ? sym[m] : ''));
940-
s = s.replace(/\\%/g, '%').replace(/\\_/g, '_').replace(/\\&/g, '&amp;').replace(/\\#/g, '#');
941-
s = s.replace(/\^\{([^{}]*)\}/g, '<sup>$1</sup>');
942-
s = s.replace(/\^([A-Za-z0-9])/g, '<sup>$1</sup>');
943-
s = s.replace(/_\{([^{}]*)\}/g, '<sub>$1</sub>');
944-
s = s.replace(/_([A-Za-z0-9])/g, '<sub>$1</sub>');
945-
s = s.replace(/[{}]/g, '');
946-
return s;
947-
}
948-
949-
function convertMathText(text) {
950-
if (text.indexOf('$') === -1) return null;
951-
const re = /\$\$([\s\S]+?)\$\$|\$([^$\n]+?)\$/g;
952-
let out = '', last = 0, m, found = false;
953-
while ((m = re.exec(text)) !== null) {
954-
found = true;
955-
out += escapeHtmlStr(text.slice(last, m.index));
956-
const tex = m[1] != null ? m[1] : m[2];
957-
out += '<span class="math">' + latexToHtml(tex) + '</span>';
958-
last = re.lastIndex;
959-
}
960-
if (!found) return null;
961-
out += escapeHtmlStr(text.slice(last));
962-
return out;
963-
}
964-
965-
function renderMathInElement(root) {
966-
if (!root) return;
967-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
968-
acceptNode(node) {
969-
if (!node.nodeValue || node.nodeValue.indexOf('$') === -1) return NodeFilter.FILTER_REJECT;
970-
let p = node.parentNode;
971-
while (p && p !== root) {
972-
const tag = p.nodeName;
973-
if (tag === 'CODE' || tag === 'PRE' || tag === 'SCRIPT' || tag === 'STYLE') return NodeFilter.FILTER_REJECT;
974-
p = p.parentNode;
975-
}
976-
return NodeFilter.FILTER_ACCEPT;
977-
}
978-
});
979-
const targets = [];
980-
let n;
981-
while ((n = walker.nextNode())) targets.push(n);
982-
targets.forEach(node => {
983-
const html = convertMathText(node.nodeValue);
984-
if (html == null) return;
985-
const span = document.createElement('span');
986-
span.innerHTML = html;
987-
node.parentNode.replaceChild(span, node);
988-
});
989-
}
990911

991912
// Handle window focus
992913
window.addEventListener('focus', () => {

0 commit comments

Comments
 (0)