11import React , { useEffect , useId , useRef , useState } from 'react'
2+ import katex from 'katex'
3+ import 'katex/dist/katex.min.css'
24import { useTheme } from '@/lib/theme'
35
46/** 极简 Markdown 渲染:标题 / 列表 / 引用 / 粗体 / [[wikilink]] / 行内代码 / 代码块 / mermaid */
57function inline ( text : string , onWikiLink ?: ( target : string ) => void ) : React . ReactNode [ ] {
68 const parts : React . ReactNode [ ] = [ ]
7- const re = / ( \[ \[ [ ^ \] ] + \] \] | \* \* [ ^ * ] + \* \* | ` [ ^ ` ] + ` ) / g
9+ const re = / ( \[ \[ [ ^ \] ] + \] \] | \\ \( [ \s \S ] + ? \\ \) | \ *\* [ ^ * ] + \* \* | ` [ ^ ` ] + ` ) / g
810 let last = 0 , m : RegExpExecArray | null , k = 0
911 while ( ( m = re . exec ( text ) ) ) {
1012 if ( m . index > last ) parts . push ( text . slice ( last , m . index ) )
@@ -36,6 +38,22 @@ function inline(text: string, onWikiLink?: (target: string) => void): React.Reac
3638 </ span >
3739 ) ,
3840 )
41+ } else if ( tok . startsWith ( '\\(' ) ) {
42+ // Inline math \( … \) via KaTeX; on a KaTeX error fall back to a code chip.
43+ const tex = tok . slice ( 2 , - 2 )
44+ let html = ''
45+ try {
46+ html = katex . renderToString ( tex , { displayMode : false , throwOnError : false } )
47+ } catch {
48+ html = ''
49+ }
50+ parts . push (
51+ html ? (
52+ < span key = { k ++ } className = "text-foreground" dangerouslySetInnerHTML = { { __html : html } } />
53+ ) : (
54+ < code key = { k ++ } className = "font-mono2 text-[12px] bg-muted rounded px-1 py-px" > { tex } </ code >
55+ ) ,
56+ )
3957 } else if ( tok . startsWith ( '**' ) ) {
4058 parts . push ( < strong key = { k ++ } className = "font-semibold text-foreground" > { tok . slice ( 2 , - 2 ) } </ strong > )
4159 } else {
@@ -61,6 +79,23 @@ function CodeBox({ code, lang }: { code: string; lang?: string }) {
6179 )
6280}
6381
82+ /** Display (block) math via KaTeX. On a KaTeX error, falls back to the raw TeX
83+ * in a code box so a malformed formula never crashes the message. */
84+ function MathBlock ( { tex } : { tex : string } ) {
85+ let html = ''
86+ try {
87+ html = katex . renderToString ( tex , { displayMode : true , throwOnError : false } )
88+ } catch {
89+ return < CodeBox code = { tex } />
90+ }
91+ return (
92+ < div
93+ className = "my-3 overflow-x-auto text-center text-foreground"
94+ dangerouslySetInnerHTML = { { __html : html } }
95+ />
96+ )
97+ }
98+
6499/**
65100 * Render a ```mermaid block as an SVG diagram. Mermaid is lazily imported (its
66101 * own bundle chunk, loaded only when a diagram appears). Theme-aware; on any
@@ -153,6 +188,30 @@ export default function MarkdownView({
153188 continue
154189 }
155190
191+ // Block math: LaTeX \[ … \] display delimiters. Handle a single-line form
192+ // and the model's usual three-line form (\[ alone, body lines, \] alone).
193+ const oneLineMath = / ^ \\ \[ ( [ \s \S ] * ?) \\ \] $ / . exec ( line . trim ( ) )
194+ if ( oneLineMath ) {
195+ flushList ( )
196+ out . push ( < MathBlock key = { key ++ } tex = { oneLineMath [ 1 ] . trim ( ) } /> )
197+ continue
198+ }
199+ if ( line . trim ( ) === '\\[' ) {
200+ flushList ( )
201+ const body : string [ ] = [ ]
202+ i ++
203+ while ( i < lines . length && lines [ i ] . trim ( ) !== '\\]' ) { body . push ( lines [ i ] ) ; i ++ }
204+ out . push ( < MathBlock key = { key ++ } tex = { body . join ( '\n' ) . trim ( ) } /> )
205+ continue
206+ }
207+
208+ // Horizontal rule / thematic break: ---, ***, or ___
209+ if ( / ^ ( - { 3 , } | \* { 3 , } | _ { 3 , } ) $ / . test ( line . trim ( ) ) ) {
210+ flushList ( )
211+ out . push ( < hr key = { key ++ } className = "my-4 border-0 border-t border-[hsl(var(--glass-border))]" /> )
212+ continue
213+ }
214+
156215 if ( line . startsWith ( '- ' ) ) { list . push ( line . slice ( 2 ) ) ; continue }
157216 flushList ( )
158217 if ( ! line . trim ( ) ) { out . push ( < div key = { key ++ } className = "h-2" /> ) ; continue }
0 commit comments