fix(ui): add KaTeX CSS and normalize LaTeX math delimiters for proper formula rendering
Import KaTeX CSS in layout.tsx to ensure math formulas display correctly in both dev and production modes. Convert LaTeX-style `\[...\]` / `\(...\)` delimiters to `$$...$$` / `$...$` since remark-math only supports dollar sign delimiters, fixing formula rendering for agents like Codex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { Metadata, Viewport } from "next"
|
||||
import "katex/dist/katex.min.css"
|
||||
import "./globals.css"
|
||||
import { JetBrains_Mono } from "next/font/google"
|
||||
import { NextIntlClientProvider } from "next-intl"
|
||||
|
||||
@@ -329,8 +329,41 @@ export type MessageResponseProps = ComponentProps<typeof Streamdown>
|
||||
const math = createMathPlugin({ singleDollarTextMath: true })
|
||||
const streamdownPlugins = { cjk, code, math, mermaid }
|
||||
|
||||
function MessageResponseImpl({ className, ...props }: MessageResponseProps) {
|
||||
// remark-math only supports `$` delimiters. Convert LaTeX-style
|
||||
// `\[...\]` / `\(...\)` to `$$...$$` / `$...$` so they are recognized.
|
||||
// Code blocks and inline code are preserved to avoid false positives.
|
||||
export function normalizeMathDelimiters(text: string): string {
|
||||
const saved: string[] = []
|
||||
const placeholder = (m: string) => {
|
||||
saved.push(m)
|
||||
return `\0CBLK${saved.length - 1}\0`
|
||||
}
|
||||
const masked = text.replace(
|
||||
/`{3,}[\s\S]*?`{3,}|~{3,}[\s\S]*?~{3,}|`[^`\n]+`/g,
|
||||
placeholder
|
||||
)
|
||||
const normalized = masked
|
||||
.replace(/\\\[([\s\S]*?)\\\]/g, (_m, inner: string) => `$$${inner}$$`)
|
||||
.replace(/\\\(([\s\S]*?)\\\)/g, (_m, inner: string) => `$${inner}$`)
|
||||
return normalized.replace(
|
||||
/\0CBLK(\d+)\0/g,
|
||||
(_m, i: string) => saved[Number(i)]
|
||||
)
|
||||
}
|
||||
|
||||
function MessageResponseImpl({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: MessageResponseProps) {
|
||||
const linkSafety = useStreamdownLinkSafety()
|
||||
const normalized = useMemo(
|
||||
() =>
|
||||
typeof children === "string"
|
||||
? normalizeMathDelimiters(children)
|
||||
: children,
|
||||
[children]
|
||||
)
|
||||
|
||||
return (
|
||||
<Streamdown
|
||||
@@ -341,7 +374,9 @@ function MessageResponseImpl({ className, ...props }: MessageResponseProps) {
|
||||
linkSafety={linkSafety}
|
||||
plugins={streamdownPlugins}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{normalized}
|
||||
</Streamdown>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Streamdown } from "streamdown"
|
||||
|
||||
import { Shimmer } from "./shimmer"
|
||||
import { useStreamdownLinkSafety } from "./link-safety"
|
||||
import { normalizeMathDelimiters } from "./message"
|
||||
|
||||
interface ReasoningContextValue {
|
||||
isStreaming: boolean
|
||||
@@ -218,6 +219,10 @@ const streamdownPlugins = { cjk, code, math, mermaid }
|
||||
export const ReasoningContent = memo(
|
||||
({ className, children, ...props }: ReasoningContentProps) => {
|
||||
const linkSafety = useStreamdownLinkSafety()
|
||||
const normalized = useMemo(
|
||||
() => normalizeMathDelimiters(children),
|
||||
[children]
|
||||
)
|
||||
|
||||
return (
|
||||
<CollapsibleContent
|
||||
@@ -233,7 +238,7 @@ export const ReasoningContent = memo(
|
||||
plugins={streamdownPlugins}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{normalized}
|
||||
</Streamdown>
|
||||
</CollapsibleContent>
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ import { createMathPlugin } from "@streamdown/math"
|
||||
import { mermaid } from "@streamdown/mermaid"
|
||||
import { Streamdown } from "streamdown"
|
||||
import { readFileBase64 } from "@/lib/api"
|
||||
import { normalizeMathDelimiters } from "@/components/ai-elements/message"
|
||||
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
||||
import "@/lib/monaco-local"
|
||||
|
||||
@@ -1354,9 +1355,8 @@ export function FileWorkspacePanel() {
|
||||
const relativeFileDir = activeFileTab.path?.includes("/")
|
||||
? activeFileTab.path.replace(/\/[^/]*$/, "")
|
||||
: ""
|
||||
const preprocessedContent = preprocessMarkdownPaths(
|
||||
renderedContent,
|
||||
relativeFileDir
|
||||
const preprocessedContent = normalizeMathDelimiters(
|
||||
preprocessMarkdownPaths(renderedContent, relativeFileDir)
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user