mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 14:38:00 +00:00
Added math rendering (#705)
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
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
This commit is contained in:
1 parent
a2bd67eda3
commit
cf6226546e
9 files changed
+684
-17
No files matched your search
@@ -150,4 +150,19 @@
|
||||
.sources-card-header {
|
||||
top: 0em !important;
|
||||
left: 2.2em !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-mathjax-block {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat-mathjax-block mjx-container[display="true"] {
|
||||
text-align: left !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.chat-mathjax-block mjx-container[display="true"] mjx-math {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
- Added the ability to format your user prompt in the chat using icons instead of typing Markdown directly.
|
||||
- Added the ability to load a system prompt from a file when creating or editing chat templates.
|
||||
- Added a start-page setting, so AI Studio can now open directly on your preferred page when the app starts. Configuration plugins can also provide and optionally lock this default for organizations.
|
||||
- Added math rendering in chats for LaTeX display formulas, including block formats such as `$$ ... $$` and `\[ ... \]`.
|
||||
- Released the document analysis assistant after an intense testing phase.
|
||||
- Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile.
|
||||
- Improved the performance by caching the OS language detection and requesting the user language only once per app start.
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user