fix(web): defer math rendering while streaming
This commit is contained in:
@@ -8,8 +8,9 @@ import { CodeBlock } from './CodeBlock.tsx'
|
|||||||
import 'katex/dist/katex.min.css'
|
import 'katex/dist/katex.min.css'
|
||||||
import css from './MarkdownText.module.css'
|
import css from './MarkdownText.module.css'
|
||||||
|
|
||||||
const remarkPlugins = [remarkGfm, remarkMath]
|
const streamingRemarkPlugins = [remarkGfm]
|
||||||
const rehypePlugins = [rehypeKatex]
|
const settledRemarkPlugins = [remarkGfm, remarkMath]
|
||||||
|
const settledRehypePlugins = [rehypeKatex]
|
||||||
|
|
||||||
function sanitizeUrl(url: string): string {
|
function sanitizeUrl(url: string): string {
|
||||||
try {
|
try {
|
||||||
@@ -92,7 +93,7 @@ const streamingComponents = buildComponents(true)
|
|||||||
/**
|
/**
|
||||||
* Render untrusted assistant-authored Markdown as semantic React elements.
|
* Render untrusted assistant-authored Markdown as semantic React elements.
|
||||||
* @param props - Markdown source text preserved by the session projection;
|
* @param props - Markdown source text preserved by the session projection;
|
||||||
* `streaming` renders fences plain (highlighting lands on the finalize swap);
|
* `streaming` renders fences and TeX plain (highlighting and KaTeX land on the finalize swap);
|
||||||
* `codeLabels` forwards localized copy-button labels to fence CodeBlocks —
|
* `codeLabels` forwards localized copy-button labels to fence CodeBlocks —
|
||||||
* pass a reference-stable object (memoized per locale revision), because the
|
* pass a reference-stable object (memoized per locale revision), because the
|
||||||
* component table memoizes on its identity and a fresh literal per render
|
* component table memoizes on its identity and a fresh literal per render
|
||||||
@@ -113,8 +114,8 @@ export function MarkdownText({ text, streaming = false, codeLabels }: {
|
|||||||
return (
|
return (
|
||||||
<div className={css.markdown}>
|
<div className={css.markdown}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={remarkPlugins}
|
remarkPlugins={streaming ? streamingRemarkPlugins : settledRemarkPlugins}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={streaming ? undefined : settledRehypePlugins}
|
||||||
components={components}
|
components={components}
|
||||||
urlTransform={safeUrl}
|
urlTransform={safeUrl}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -144,6 +144,21 @@ describe('MarkdownText', () => {
|
|||||||
expect(container.querySelector('.katex-display annotation')?.textContent).toContain('\\frac{\\partial \\mathbf{u}}')
|
expect(container.querySelector('.katex-display annotation')?.textContent).toContain('\\frac{\\partial \\mathbf{u}}')
|
||||||
expect(container.querySelector('a')).toBeNull()
|
expect(container.querySelector('a')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('defers TeX rendering while streaming so incomplete formulas never flash KaTeX errors', () => {
|
||||||
|
const partial = '$$\n\\frac{\\partial \\mathbf{u}}{\\partial'
|
||||||
|
const complete = '$$\n\\frac{\\partial \\mathbf{u}}{\\partial t}\n$$'
|
||||||
|
const live = render(<MarkdownText text={partial} streaming />)
|
||||||
|
|
||||||
|
expect(live.container.querySelector('.katex')).toBeNull()
|
||||||
|
expect(live.container.querySelector('.katex-error')).toBeNull()
|
||||||
|
expect(live.container.textContent).toContain('\\frac{\\partial \\mathbf{u}}{\\partial')
|
||||||
|
|
||||||
|
live.rerender(<MarkdownText text={complete} />)
|
||||||
|
expect(live.container.querySelectorAll('.katex')).toHaveLength(1)
|
||||||
|
expect(live.container.querySelectorAll('.katex-display')).toHaveLength(1)
|
||||||
|
expect(live.container.querySelector('.katex-error')).toBeNull()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('JsonBlock', () => {
|
describe('JsonBlock', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user