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 (