feat(web): add LaTeX math formula rendering with KaTeX (#436)

* feat(web): add LaTeX math formula rendering with KaTeX

Add remark-math + rehype-katex to the markdown rendering pipeline
so inline ($...$) and display ($$...$$) math formulas are rendered
as proper KaTeX output in chat messages and tool results.

Closes #237

* fix(web): disable single-dollar math parsing and add KaTeX to reasoning

- Set singleDollarTextMath: false to prevent $HOME, $PATH etc from
  being misinterpreted as math formulas. Only $$...$$ (display) is
  parsed; inline math requires explicit \(...\) or $$...$$.
- Add rehypePlugins to the reasoning renderer so math formulas
  render consistently across chat, tool results, and reasoning blocks.

* refactor(web): use satisfies for type-safe plugin exports

Replace any[] with satisfies NonNullable<MarkdownTextPrimitiveProps[...]>
to preserve type safety on the shared plugin lists without needing
eslint suppressions.

* fix(web): enable single-dollar inline math syntax

Re-enable $...$ parsing (remark-math default) so inline formulas
like $E=mc^2$ render correctly. Shell variables like $HOME typically
appear inside code spans/blocks which remark-math does not parse,
so false positives are minimal in practice.
This commit is contained in:
Haoqing Wang
2026-04-11 22:09:37 +08:00
committed by GitHub
parent 9a48d5af3a
commit 813ac7fdda
6 changed files with 31 additions and 14 deletions
+2 -1
View File
@@ -1,7 +1,7 @@
import type { MarkdownTextPrimitiveProps } from '@assistant-ui/react-markdown'
import { MarkdownTextPrimitive } from '@assistant-ui/react-markdown'
import { TextMessagePartProvider } from '@assistant-ui/react'
import { MARKDOWN_PLUGINS, defaultComponents } from '@/components/assistant-ui/markdown-text'
import { MARKDOWN_PLUGINS, MARKDOWN_REHYPE_PLUGINS, defaultComponents } from '@/components/assistant-ui/markdown-text'
import { cn } from '@/lib/utils'
interface MarkdownRendererProps {
@@ -18,6 +18,7 @@ function MarkdownContent(props: MarkdownRendererProps) {
<TextMessagePartProvider text={props.content}>
<MarkdownTextPrimitive
remarkPlugins={MARKDOWN_PLUGINS}
rehypePlugins={MARKDOWN_REHYPE_PLUGINS}
components={mergedComponents}
className={cn('aui-md min-w-0 max-w-full break-words text-base')}
/>
@@ -6,13 +6,18 @@ import {
type CodeHeaderProps,
} from '@assistant-ui/react-markdown'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import remarkDisableIndentedCode from '@/lib/remark-disable-indented-code'
import { cn } from '@/lib/utils'
import { SyntaxHighlighter } from '@/components/assistant-ui/shiki-highlighter'
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'
import { CopyIcon, CheckIcon } from '@/components/icons'
export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode]
import type { MarkdownTextPrimitiveProps } from '@assistant-ui/react-markdown'
export const MARKDOWN_PLUGINS = [remarkGfm, remarkMath, remarkDisableIndentedCode] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>
export const MARKDOWN_REHYPE_PLUGINS = [rehypeKatex] satisfies NonNullable<MarkdownTextPrimitiveProps['rehypePlugins']>
function CodeHeader(props: CodeHeaderProps) {
const { copied, copy } = useCopyToClipboard()
@@ -225,6 +230,7 @@ export function MarkdownText() {
return (
<MarkdownTextPrimitive
remarkPlugins={MARKDOWN_PLUGINS}
rehypePlugins={MARKDOWN_REHYPE_PLUGINS}
components={defaultComponents}
className={cn('aui-md min-w-0 max-w-full break-words text-base')}
/>
@@ -2,7 +2,7 @@ import { useState, useEffect, type FC, type PropsWithChildren } from 'react'
import { useMessage } from '@assistant-ui/react'
import { MarkdownTextPrimitive } from '@assistant-ui/react-markdown'
import { cn } from '@/lib/utils'
import { defaultComponents, MARKDOWN_PLUGINS } from '@/components/assistant-ui/markdown-text'
import { defaultComponents, MARKDOWN_PLUGINS, MARKDOWN_REHYPE_PLUGINS } from '@/components/assistant-ui/markdown-text'
function ChevronIcon(props: { className?: string; open?: boolean }) {
return (
@@ -40,6 +40,7 @@ export const Reasoning: FC = () => {
return (
<MarkdownTextPrimitive
remarkPlugins={MARKDOWN_PLUGINS}
rehypePlugins={MARKDOWN_REHYPE_PLUGINS}
components={defaultComponents}
className={cn('aui-reasoning-content min-w-0 max-w-full break-words text-sm text-[var(--app-hint)]')}
/>