Skip to content

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

Merged
hqhq1025 merged 4 commits intotiann:mainfrom
hqhq1025:feat/web-katex-math-rendering
Apr 11, 2026
Merged

feat(web): add LaTeX math formula rendering with KaTeX#436
hqhq1025 merged 4 commits intotiann:mainfrom
hqhq1025:feat/web-katex-math-rendering

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Summary

Add KaTeX-based LaTeX formula rendering to the web chat UI. Both inline ($E=mc^2$) and display ($$\int_0^1 x^2 dx$$) math syntax are now rendered as formatted formulas.

Changes

  • Add remark-math, rehype-katex, and katex dependencies
  • Add remarkMath to the remark plugin pipeline and rehypeKatex to the rehype plugin pipeline
  • Import KaTeX stylesheet in index.css
  • Both MarkdownText (chat messages) and MarkdownRenderer (tool results) share the same plugin config

How it works

  1. remark-math parses $...$ (inline) and $$...$$ (display) syntax into math AST nodes
  2. rehype-katex renders those nodes into KaTeX HTML
  3. KaTeX CSS styles the rendered output

This is the same standard approach used by Claude.ai and ChatGPT.

Before After
$E=mc^2$ shown as raw text $E=mc^2$ rendered as formula
$$\int_0^1 x^2 dx$$ shown as raw text Rendered as display math

Closes #237

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 tiann#237
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Single-dollar math is now enabled on every shared markdown surface, including arbitrary tool/result text rendered through MarkdownRenderer, so common coding strings such as export PATH=$HOME/bin:$PATH can be reinterpreted as math instead of literal text. Evidence web/src/components/assistant-ui/markdown-text.tsx:17, web/src/components/MarkdownRenderer.tsx:21, web/src/components/ToolCard/views/_results.tsx:129, web/src/components/ToolCard/views/_results.tsx:137.
    Suggested fix:
    export const MARKDOWN_PLUGINS = [
        remarkGfm,
        [remarkMath, { singleDollarTextMath: false }],
        remarkDisableIndentedCode,
    ]
  • [Minor] The shared plugin list now adds remarkMath, but the reasoning renderer still only passes remarkPlugins, so formulas in reasoning blocks will not render the same way as the main chat/tool markdown. Evidence web/src/components/assistant-ui/markdown-text.tsx:17, web/src/components/assistant-ui/reasoning.tsx:41.
    Suggested fix:
    <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)]')}
    />

Summary
Review mode: initial. Two regressions introduced by the shared KaTeX rollout: single-dollar parsing now applies to arbitrary tool/result text, and the reasoning renderer was not updated to use the matching rehype pipeline.

Testing

  • Not run (automation; PR content treated as untrusted). Suggested coverage: markdown snapshots for $HOME:$PATH, $5, $$x^2$$, and reasoning messages containing math.

HAPI Bot

import { CopyIcon, CheckIcon } from '@/components/icons'

export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode]
export const MARKDOWN_PLUGINS = [remarkGfm, remarkMath, remarkDisableIndentedCode]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] This shared plugin list now affects MarkdownRenderer too, and that renderer is fed arbitrary tool/result text (web/src/components/ToolCard/views/_results.tsx:129-137). With default remark-math settings, common coding strings such as export PATH=$HOME/bin:$PATH or paired dollar amounts can be parsed as math instead of literal text. If you want math here, please at least disable single-dollar parsing.

Suggested fix:

export const MARKDOWN_PLUGINS = [
    remarkGfm,
    [remarkMath, { singleDollarTextMath: false }],
    remarkDisableIndentedCode,
]


export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode]
export const MARKDOWN_PLUGINS = [remarkGfm, remarkMath, remarkDisableIndentedCode]
export const MARKDOWN_REHYPE_PLUGINS = [rehypeKatex]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] The shared math pipeline is only wired into MarkdownText and MarkdownRenderer. web/src/components/assistant-ui/reasoning.tsx:41-44 still passes remarkPlugins={MARKDOWN_PLUGINS} without the matching rehypePlugins, so reasoning blocks will render math differently from the rest of the chat UI.

Suggested fix:

<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)]')}
/>

- 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.
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Minor] Shared markdown plugin exports now use any[] plus @typescript-eslint/no-explicit-any suppressions, which drops type safety on a hot path reused by chat, reasoning, and MarkdownRenderer. That makes future plugin tuple mistakes invisible to the compiler. Evidence web/src/components/assistant-ui/markdown-text.tsx:17, web/src/components/assistant-ui/markdown-text.tsx:20.
    Suggested fix:
    import {
        MarkdownTextPrimitive,
        unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
        useIsMarkdownCodeBlock,
        type CodeHeaderProps,
        type MarkdownTextPrimitiveProps,
    } from '@assistant-ui/react-markdown'
    
    export const MARKDOWN_PLUGINS = [
        remarkGfm,
        [remarkMath, { singleDollarTextMath: false }],
        remarkDisableIndentedCode,
    ] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>
    
    export const MARKDOWN_REHYPE_PLUGINS = [
        rehypeKatex,
    ] satisfies NonNullable<MarkdownTextPrimitiveProps['rehypePlugins']>

Summary
Review mode: follow-up after new commits. The follow-up resolves the earlier math-rendering regressions, but it now weakens the shared markdown pipeline by exporting the plugin lists as any[] with lint suppressions on web/src/components/assistant-ui/markdown-text.tsx:17 and web/src/components/assistant-ui/markdown-text.tsx:20.

Testing

  • Not run (automation; PR content treated as untrusted). Suggested coverage: renderer snapshots for inline math, display math, and literal shell/currency strings across chat, reasoning, and MarkdownRenderer.

HAPI Bot


export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const MARKDOWN_PLUGINS: any[] = [remarkGfm, [remarkMath, { singleDollarTextMath: false }], remarkDisableIndentedCode]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] These shared markdown plugin exports are now typed as any[] with @typescript-eslint/no-explicit-any suppressions. That sidesteps the repo's strict TypeScript checks on a pipeline reused by chat, reasoning, and MarkdownRenderer, so future plugin tuple mistakes will be invisible to the compiler.

Suggested fix:

import {
    MarkdownTextPrimitive,
    unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
    useIsMarkdownCodeBlock,
    type CodeHeaderProps,
    type MarkdownTextPrimitiveProps,
} from '@assistant-ui/react-markdown'

export const MARKDOWN_PLUGINS = [
    remarkGfm,
    [remarkMath, { singleDollarTextMath: false }],
    remarkDisableIndentedCode,
] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>

export const MARKDOWN_REHYPE_PLUGINS = [
    rehypeKatex,
] satisfies NonNullable<MarkdownTextPrimitiveProps['rehypePlugins']>

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

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Single-dollar inline math is still disabled, so the new markdown pipeline only renders formulas when users wrap inline expressions in two or more dollar signs. Most LLM markdown and existing KaTeX/remark-math examples use $...$, so expressions like $E=mc^2$ will keep showing up as raw text. Evidence web/src/components/assistant-ui/markdown-text.tsx:19.
    Suggested fix:
    export const MARKDOWN_PLUGINS = [
        remarkGfm,
        remarkMath,
        remarkDisableIndentedCode,
    ] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>

Summary
Review mode: follow-up after new commits. The previous typing issue is fixed, but the current parser config in web/src/components/assistant-ui/markdown-text.tsx:19 still blocks the dominant inline math syntax ($...$), so the feature remains only partially implemented.

Testing

  • Not run (automation). Suggested coverage: inline $...$, display $$...$$, and literal currency/shell strings across chat, reasoning, and MarkdownRenderer.

HAPI Bot

export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode]
import type { MarkdownTextPrimitiveProps } from '@assistant-ui/react-markdown'

export const MARKDOWN_PLUGINS = [remarkGfm, [remarkMath, { singleDollarTextMath: false }], remarkDisableIndentedCode] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] remark-math is configured with singleDollarTextMath: false here, which means the shared markdown pipeline will not parse the dominant inline syntax $...$. With this setting, examples like $E=mc^2$ still render as plain text in chat, reasoning, and tool-result markdown.

Suggested fix:

export const MARKDOWN_PLUGINS = [
    remarkGfm,
    remarkMath,
    remarkDisableIndentedCode,
] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>

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.
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] KaTeX math will inherit the repo's existing code/pre chrome, so formulas render as inline-code pills or code blocks instead of plain math. Evidence web/src/components/assistant-ui/markdown-text.tsx:20, web/src/components/assistant-ui/markdown-text.tsx:43, web/src/components/assistant-ui/markdown-text.tsx:59.

Summary
Review mode: follow-up after new commits. The previous single-dollar parser issue is fixed, but the new rehype-katex hookup still collides with the custom code and pre renderers, so math output is styled like code in chat, tool, and reasoning markdown.

Testing

  • Not run (automation; bun not installed in this runner). Suggested coverage: inline $...$ and display $$...$$ render paths in MarkdownText, MarkdownRenderer, and Reasoning, with assertions that math output is not wrapped in the inline-code or code-block UI.

HAPI Bot

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']>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] rehype-katex renders math into code.math-inline and pre > code.math-display, but this file still routes every code/pre node through the repo's inline-code and code-block components. That means inline formulas pick up the gray code pill and display formulas pick up the code-block wrapper instead of plain KaTeX. Evidence: web/src/components/assistant-ui/markdown-text.tsx:20, web/src/components/assistant-ui/markdown-text.tsx:43, web/src/components/assistant-ui/markdown-text.tsx:59.

Suggested fix:

function Code(props: ComponentPropsWithoutRef<'code'>) {
    if (props.className?.includes('math-inline') || props.className?.includes('math-display')) {
        return <code {...props} />
    }

    const isCodeBlock = useIsMarkdownCodeBlock()
    // existing code styling...
}

function Pre(props: ComponentPropsWithoutRef<'pre'>) {
    const child = React.Children.only(props.children)

    if (React.isValidElement(child) && typeof child.props.className === 'string' && child.props.className.includes('math-display')) {
        return <div className="max-w-full overflow-x-auto">{props.children}</div>
    }

    // existing code-block wrapper...
}

@hqhq1025 hqhq1025 merged commit 813ac7f into tiann:main Apr 11, 2026
2 checks passed
Hwwwww-dev pushed a commit to Hwwwww-dev/hapi that referenced this pull request Apr 17, 2026
* 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 tiann#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

希望能在前端页面加一个latex公式渲染的能力,比如接入mathjax

1 participant