diff --git a/frontend/src/components/ui/markdown-editor.test.tsx b/frontend/src/components/ui/markdown-editor.test.tsx
index 0015cea09b..05f4982235 100644
--- a/frontend/src/components/ui/markdown-editor.test.tsx
+++ b/frontend/src/components/ui/markdown-editor.test.tsx
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest'
+import { describe, it, expect, vi, afterEach } from 'vitest'
import { render } from '@testing-library/react'
import MarkdownPreview from '@uiw/react-markdown-preview'
@@ -58,6 +58,37 @@ describe('MarkdownEditor preview sanitization', () => {
expect(container.querySelector('.katex-mathml math')).not.toBeNull()
})
+ describe('KATEX_OPTIONS unknownSymbol suppression', () => {
+ // KaTeX's default strict mode warns on every Unicode character outside
+ // its symbol table. AI-generated content routinely has one inside a
+ // legitimate single-dollar math span (this project's prompts steer
+ // models to emit inline math as $...$) - e.g. an en-dash in a price
+ // range. That specific warning is intentionally suppressed; every other
+ // strict warning is not, so a future edit that widens the ignore list
+ // (or removes it) shows up here instead of silently regressing.
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it('does not warn on the em/en-dash-in-price-range pattern that motivated this fix', () => {
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+ renderPreview('Price range: $5 – $10')
+ const unknownSymbolWarnings = warn.mock.calls.filter(([msg]) =>
+ typeof msg === 'string' && msg.includes('[unknownSymbol]')
+ )
+ expect(unknownSymbolWarnings).toHaveLength(0)
+ })
+
+ it('still warns on a different strict violation (comment with no terminating newline)', () => {
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+ renderPreview('Still checked: $x^2 %comment$')
+ const commentWarnings = warn.mock.calls.filter(([msg]) =>
+ typeof msg === 'string' && msg.includes('[commentAtEnd]')
+ )
+ expect(commentWarnings.length).toBeGreaterThan(0)
+ })
+ })
+
it('still syntax-highlights fenced code blocks', () => {
const { container } = renderPreview('```python\ndef hello():\n return 42\n```')
expect(container.querySelectorAll('span[class*="token"]').length).toBeGreaterThan(0)
diff --git a/frontend/src/components/ui/markdown-editor.tsx b/frontend/src/components/ui/markdown-editor.tsx
index e6caeff4bf..471d4ac173 100644
--- a/frontend/src/components/ui/markdown-editor.tsx
+++ b/frontend/src/components/ui/markdown-editor.tsx
@@ -7,6 +7,8 @@ import rehypeKatex from 'rehype-katex'
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
import type { PluggableList } from 'unified'
+import { KATEX_OPTIONS } from '@/lib/utils/katex-options'
+
const MDEditor = dynamic(
() => import('@uiw/react-md-editor').then((mod) => mod.default),
{ ssr: false }
@@ -46,7 +48,7 @@ const SANITIZE_SCHEMA = {
export const PREVIEW_OPTIONS = {
remarkPlugins: [remarkMath] as PluggableList,
- rehypePlugins: [[rehypeSanitize, SANITIZE_SCHEMA], rehypeKatex] as PluggableList,
+ rehypePlugins: [[rehypeSanitize, SANITIZE_SCHEMA], [rehypeKatex, KATEX_OPTIONS]] as PluggableList,
}
export interface MarkdownEditorProps {
diff --git a/frontend/src/components/ui/markdown-renderer.tsx b/frontend/src/components/ui/markdown-renderer.tsx
index 212e2b1089..c8320c9265 100644
--- a/frontend/src/components/ui/markdown-renderer.tsx
+++ b/frontend/src/components/ui/markdown-renderer.tsx
@@ -6,6 +6,7 @@ import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import { useThemeStore } from '@/lib/stores/theme-store'
+import { KATEX_OPTIONS } from '@/lib/utils/katex-options'
import { oneDark as darkTheme } from 'react-syntax-highlighter/dist/esm/styles/prism'
import { oneLight as lightTheme } from 'react-syntax-highlighter/dist/esm/styles/prism'
// PrismLight with an explicit language set instead of the full Prism build:
@@ -63,7 +64,7 @@ export function MarkdownRenderer({ children, components = {}}: { children: React
{children}
,
h1: ({ children }) => {children}
,
diff --git a/frontend/src/lib/utils/katex-options.ts b/frontend/src/lib/utils/katex-options.ts
new file mode 100644
index 0000000000..d72e767516
--- /dev/null
+++ b/frontend/src/lib/utils/katex-options.ts
@@ -0,0 +1,17 @@
+import type { KatexOptions } from 'katex'
+
+// AI-generated content often contains Unicode punctuation (en/em dashes,
+// smart quotes) inside single-dollar math spans that models emit for
+// legitimate inline math (see prompts/*/system.jinja "MATH FORMATTING"
+// sections). KaTeX's default strict mode logs a console warning for each
+// character outside its symbol table. It still renders - falling back to
+// text-mode handling for that character (see katex's Parser: unknown
+// codepoints get `mode: 'text'` instead of proper math-mode metrics) - so
+// this is not universally cosmetic: an unrecognized symbol can come out
+// with approximated spacing/metrics rather than a "real" glyph. Acceptable
+// for stray punctuation like a dash; don't extend this ignore to justify
+// dumping arbitrary Unicode into math mode. Every other strict check (e.g.
+// deprecated commands) is untouched.
+export const KATEX_OPTIONS: KatexOptions = {
+ strict: (errorCode) => (errorCode === 'unknownSymbol' ? 'ignore' : 'warn'),
+}