diff --git a/crates/openfang-api/src/webchat.rs b/crates/openfang-api/src/webchat.rs
index ec8ada30e..d37e014c9 100644
--- a/crates/openfang-api/src/webchat.rs
+++ b/crates/openfang-api/src/webchat.rs
@@ -127,6 +127,8 @@ const WEBCHAT_HTML: &str = concat!(
"\n",
include_str!("../static/js/pages/overview.js"),
"\n",
+ include_str!("../static/js/katex.js"),
+ "\n",
include_str!("../static/js/pages/chat.js"),
"\n",
include_str!("../static/js/pages/agents.js"),
diff --git a/crates/openfang-api/static/index_head.html b/crates/openfang-api/static/index_head.html
index e052c081e..7bea95786 100644
--- a/crates/openfang-api/static/index_head.html
+++ b/crates/openfang-api/static/index_head.html
@@ -11,7 +11,4 @@
-
-
-
diff --git a/crates/openfang-api/static/js/app.js b/crates/openfang-api/static/js/app.js
index 6e1132c32..e30a80b41 100644
--- a/crates/openfang-api/static/js/app.js
+++ b/crates/openfang-api/static/js/app.js
@@ -66,26 +66,6 @@ function renderMarkdown(text) {
return escapeHtml(text);
}
-// Render LaTeX math in the chat message container using KaTeX auto-render.
-// Call this after new messages are inserted into the DOM.
-function renderLatex(el) {
- if (typeof renderMathInElement !== 'function') return;
- var target = el || document.getElementById('messages');
- if (!target) return;
- try {
- renderMathInElement(target, {
- delimiters: [
- { left: '$$', right: '$$', display: true },
- { left: '\\[', right: '\\]', display: true },
- { left: '$', right: '$', display: false },
- { left: '\\(', right: '\\)', display: false }
- ],
- throwOnError: false,
- trust: false
- });
- } catch(e) { /* KaTeX render error — ignore gracefully */ }
-}
-
function copyCode(btn) {
var code = btn.nextElementSibling;
if (code) {
diff --git a/crates/openfang-api/static/js/katex.js b/crates/openfang-api/static/js/katex.js
new file mode 100644
index 000000000..8a704ade8
--- /dev/null
+++ b/crates/openfang-api/static/js/katex.js
@@ -0,0 +1,84 @@
+// On-demand KaTeX loader and renderer for chat messages.
+
+var KATEX_VERSION = '0.16.21';
+var KATEX_CSS_URL = 'https://cdn.jsdelivr.net/npm/katex@' + KATEX_VERSION + '/dist/katex.min.css';
+var KATEX_JS_URL = 'https://cdn.jsdelivr.net/npm/katex@' + KATEX_VERSION + '/dist/katex.min.js';
+var KATEX_AUTORENDER_URL =
+ 'https://cdn.jsdelivr.net/npm/katex@' + KATEX_VERSION + '/dist/contrib/auto-render.min.js';
+var katexLoadPromise = null;
+
+function hasLatexDelimiters(text) {
+ if (!text) return false;
+ return /\$\$|\\\[|\\\(|\$(?=\S)[^$\n]+\$/.test(text);
+}
+
+function loadScript(url) {
+ return new Promise(function (resolve, reject) {
+ var script = document.createElement('script');
+ script.src = url;
+ script.async = true;
+ script.onload = function () {
+ resolve();
+ };
+ script.onerror = function () {
+ reject(new Error('Failed to load script: ' + url));
+ };
+ document.head.appendChild(script);
+ });
+}
+
+function ensureKatexLoaded() {
+ if (typeof renderMathInElement === 'function') return Promise.resolve(true);
+ if (katexLoadPromise) return katexLoadPromise;
+
+ katexLoadPromise = new Promise(function (resolve) {
+ var cssId = 'openfang-katex-css';
+ if (!document.getElementById(cssId)) {
+ var link = document.createElement('link');
+ link.id = cssId;
+ link.rel = 'stylesheet';
+ link.href = KATEX_CSS_URL;
+ document.head.appendChild(link);
+ }
+
+ loadScript(KATEX_JS_URL)
+ .then(function () {
+ return loadScript(KATEX_AUTORENDER_URL);
+ })
+ .then(function () {
+ resolve(typeof renderMathInElement === 'function');
+ })
+ .catch(function () {
+ katexLoadPromise = null;
+ resolve(false);
+ });
+ });
+
+ return katexLoadPromise;
+}
+
+// Render LaTeX math in the chat message container using KaTeX auto-render.
+// Call this after new messages are inserted into the DOM.
+function renderLatex(el) {
+ var target = el || document.getElementById('messages');
+ if (!target) return;
+ if (!hasLatexDelimiters(target.textContent || '')) return;
+
+ ensureKatexLoaded().then(function (ok) {
+ if (!ok || typeof renderMathInElement !== 'function') return;
+ try {
+ renderMathInElement(target, {
+ delimiters: [
+ { left: '$$', right: '$$', display: true },
+ { left: '\\[', right: '\\]', display: true },
+ { left: '$', right: '$', display: false },
+ { left: '\\(', right: '\\)', display: false },
+ ],
+ throwOnError: false,
+ trust: false,
+ });
+ } catch (e) {
+ /* KaTeX render error — ignore gracefully */
+ }
+ });
+}