AI-Studio/app/MindWork AI Studio/wwwroot/chat-math.js
Thorsten Sommer cf6226546e
Some checks are pending
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Added math rendering (#705)
2026-03-21 20:34:11 +01:00

287 lines
8.3 KiB
JavaScript

const MATH_JAX_SCRIPT_ID = 'mudblazor-markdown-mathjax'
const MATH_JAX_SCRIPT_SRC = '_content/MudBlazor.Markdown/MudBlazor.Markdown.MathJax.min.js'
const INTERSECTION_ROOT_MARGIN = '240px 0px 240px 0px'
const MAX_TYPES_PER_BATCH = 4
const containerStates = new Map()
const pendingMathElements = new Set()
let mathJaxReadyPromise = null
let batchScheduled = false
let typesetInProgress = false
function applyMathJaxConfiguration() {
window.MathJax = window.MathJax ?? {}
window.MathJax.options = window.MathJax.options ?? {}
window.MathJax.options.enableMenu = false
}
function isMathJaxReady() {
return typeof window.MathJax?.typesetPromise === 'function' || typeof window.MathJax?.typeset === 'function'
}
function waitForMathJaxReady(attempt = 0) {
if (isMathJaxReady())
return Promise.resolve()
if (attempt >= 80)
return Promise.reject(new Error('MathJax did not finish loading in time.'))
return new Promise((resolve, reject) => {
window.setTimeout(() => {
waitForMathJaxReady(attempt + 1).then(resolve).catch(reject)
}, 50)
})
}
function ensureMathJaxLoaded() {
if (isMathJaxReady())
return Promise.resolve()
if (mathJaxReadyPromise)
return mathJaxReadyPromise
mathJaxReadyPromise = new Promise((resolve, reject) => {
applyMathJaxConfiguration()
let script = document.getElementById(MATH_JAX_SCRIPT_ID)
const onLoad = () => {
waitForMathJaxReady().then(resolve).catch(reject)
}
const onError = () => reject(new Error('Failed to load the MathJax script.'))
if (!script) {
script = document.createElement('script')
script.id = MATH_JAX_SCRIPT_ID
script.type = 'text/javascript'
script.src = MATH_JAX_SCRIPT_SRC
script.addEventListener('load', onLoad, { once: true })
script.addEventListener('error', onError, { once: true })
document.head.appendChild(script)
return
}
script.addEventListener('load', onLoad, { once: true })
script.addEventListener('error', onError, { once: true })
void waitForMathJaxReady().then(resolve).catch(() => {})
}).catch(error => {
mathJaxReadyPromise = null
throw error
})
return mathJaxReadyPromise
}
function createContainerState() {
return {
signature: '',
observer: null,
observedElements: new Set()
}
}
function disconnectContainerState(state) {
if (state.observer) {
state.observer.disconnect()
state.observer = null
}
for (const element of state.observedElements)
pendingMathElements.delete(element)
state.observedElements.clear()
}
function isNearViewport(element) {
const rect = element.getBoundingClientRect()
return rect.bottom >= -240 && rect.top <= window.innerHeight + 240
}
function queueElementForTypeset(element, signature) {
if (!element || !element.isConnected)
return
if (element.dataset.chatMathProcessedSignature === signature)
return
element.dataset.chatMathTargetSignature = signature
element.dataset.chatMathPending = 'true'
pendingMathElements.add(element)
schedulePendingTypeset(false)
}
function schedulePendingTypeset(useIdleCallback) {
if (batchScheduled)
return
batchScheduled = true
const flush = () => {
batchScheduled = false
void flushPendingTypeset()
}
if (useIdleCallback && typeof window.requestIdleCallback === 'function') {
window.requestIdleCallback(flush, { timeout: 120 })
return
}
window.requestAnimationFrame(flush)
}
async function flushPendingTypeset() {
if (typesetInProgress || pendingMathElements.size === 0)
return
typesetInProgress = true
const elementsToTypeset = []
try {
await ensureMathJaxLoaded()
for (const element of pendingMathElements) {
if (elementsToTypeset.length >= MAX_TYPES_PER_BATCH)
break
if (!element.isConnected) {
pendingMathElements.delete(element)
continue
}
const targetSignature = element.dataset.chatMathTargetSignature ?? ''
if (element.dataset.chatMathProcessedSignature === targetSignature) {
pendingMathElements.delete(element)
element.dataset.chatMathPending = 'false'
continue
}
elementsToTypeset.push(element)
}
if (elementsToTypeset.length === 0)
return
for (const element of elementsToTypeset)
pendingMathElements.delete(element)
if (typeof window.MathJax?.typesetClear === 'function') {
try {
window.MathJax.typesetClear(elementsToTypeset)
} catch (error) {
console.warn('chatMath: failed to clear previous MathJax state.', error)
}
}
if (typeof window.MathJax?.typesetPromise === 'function')
await window.MathJax.typesetPromise(elementsToTypeset)
else if (typeof window.MathJax?.typeset === 'function')
window.MathJax.typeset(elementsToTypeset)
for (const element of elementsToTypeset) {
element.dataset.chatMathProcessedSignature = element.dataset.chatMathTargetSignature ?? ''
element.dataset.chatMathPending = 'false'
}
} catch (error) {
console.warn('chatMath: failed to typeset math content.', error)
for (const element of elementsToTypeset)
if (element.isConnected)
pendingMathElements.add(element)
} finally {
typesetInProgress = false
if (pendingMathElements.size > 0)
schedulePendingTypeset(true)
}
}
function createIntersectionObserver(state, signature) {
return new IntersectionObserver(entries => {
let queuedVisibleElement = false
for (const entry of entries) {
if (!entry.isIntersecting)
continue
const element = entry.target
state.observer?.unobserve(element)
state.observedElements.delete(element)
queueElementForTypeset(element, signature)
queuedVisibleElement = true
}
if (queuedVisibleElement)
schedulePendingTypeset(true)
}, {
root: null,
rootMargin: INTERSECTION_ROOT_MARGIN,
threshold: 0.01
})
}
function getMathElements(container) {
return Array.from(container.querySelectorAll('.chat-mathjax-block'))
}
window.chatMath = {
syncContainer: async function(container, signature) {
if (!container)
return
let state = containerStates.get(container)
if (!state) {
state = createContainerState()
containerStates.set(container, state)
}
if (state.signature === signature)
return
disconnectContainerState(state)
state.signature = signature
const mathElements = getMathElements(container)
if (mathElements.length === 0)
return
await ensureMathJaxLoaded()
state.observer = createIntersectionObserver(state, signature)
for (const element of mathElements) {
if (isNearViewport(element)) {
queueElementForTypeset(element, signature)
continue
}
element.dataset.chatMathTargetSignature = signature
state.observer.observe(element)
state.observedElements.add(element)
}
schedulePendingTypeset(false)
},
disposeContainer: function(container) {
if (!container)
return
const state = containerStates.get(container)
if (!state)
return
disconnectContainerState(state)
containerStates.delete(container)
const mathElements = getMathElements(container)
for (const element of mathElements)
pendingMathElements.delete(element)
if (typeof window.MathJax?.typesetClear === 'function' && mathElements.length > 0) {
try {
window.MathJax.typesetClear(mathElements)
} catch (error) {
console.warn('chatMath: failed to clear container MathJax state during dispose.', error)
}
}
}
}