From c3671173923b0e80bd9f727de97b9d26a33300d9 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 8 Apr 2026 23:27:40 +0800 Subject: [PATCH] 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) --- src/app/layout.tsx | 1 + src/components/ai-elements/message.tsx | 39 ++++++++++++++++++- src/components/ai-elements/reasoning.tsx | 7 +++- src/components/files/file-workspace-panel.tsx | 6 +-- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e142808..b1905e7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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" diff --git a/src/components/ai-elements/message.tsx b/src/components/ai-elements/message.tsx index 3d5c9ad..b17afbf 100644 --- a/src/components/ai-elements/message.tsx +++ b/src/components/ai-elements/message.tsx @@ -329,8 +329,41 @@ export type MessageResponseProps = ComponentProps 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 ( + > + {normalized} + ) } diff --git a/src/components/ai-elements/reasoning.tsx b/src/components/ai-elements/reasoning.tsx index 4410fd0..3ed2503 100644 --- a/src/components/ai-elements/reasoning.tsx +++ b/src/components/ai-elements/reasoning.tsx @@ -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 ( - {children} + {normalized} ) diff --git a/src/components/files/file-workspace-panel.tsx b/src/components/files/file-workspace-panel.tsx index 1da34c3..7dd724c 100644 --- a/src/components/files/file-workspace-panel.tsx +++ b/src/components/files/file-workspace-panel.tsx @@ -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 (