1
- import { FC , memo , ComponentType } from 'react'
2
- import ReactMarkdown , { Options } from 'react-markdown'
1
+ import { ComponentType , FC , memo } from 'react'
2
+ import ReactMarkdown , { Components , Options } from 'react-markdown'
3
3
import rehypeKatex from 'rehype-katex'
4
4
import remarkGfm from 'remark-gfm'
5
5
import remarkMath from 'remark-math'
6
- import { CodeBlock } from './codeblock'
7
- import { DocumentInfo } from './document-info'
6
+ import { cn } from '../lib/utils'
8
7
import { SourceData } from './chat-sources'
9
8
import { Citation , CitationComponentProps } from './citation'
10
- import { cn } from '../lib/utils'
9
+ import { CodeBlock } from './codeblock'
10
+ import { DocumentInfo } from './document-info'
11
11
12
12
const MemoizedReactMarkdown : FC < Options > = memo (
13
13
ReactMarkdown ,
@@ -110,21 +110,38 @@ export interface LanguageRendererProps {
110
110
className ?: string
111
111
}
112
112
113
+ type ReactStyleMarkdownComponents = {
114
+ // Extract pulls out the ComponentType side of unions like ComponentType | string
115
+ // react-markdown supports passing "h1" for example, which is difficult to
116
+ [ K in keyof Components ] ?: Extract < Components [ K ] , FC < any > >
117
+ }
118
+
119
+ // Simple function to render a component if provided, otherwise use fallback
120
+ function combineComponent < Props > (
121
+ component : FC < Props > | undefined ,
122
+ fallback : FC < Props >
123
+ ) : FC < Props > {
124
+ return props => component ?.( props ) || fallback ( props )
125
+ }
126
+
127
+ export interface MarkdownProps {
128
+ content : string
129
+ sources ?: SourceData
130
+ backend ?: string
131
+ components ?: ReactStyleMarkdownComponents
132
+ citationComponent ?: ComponentType < CitationComponentProps >
133
+ className ?: string
134
+ languageRenderers ?: Record < string , ComponentType < LanguageRendererProps > >
135
+ }
113
136
export function Markdown ( {
114
137
content,
115
138
sources,
116
139
backend,
117
140
citationComponent : CitationComponent ,
118
141
className : customClassName ,
142
+ components,
119
143
languageRenderers,
120
- } : {
121
- content : string
122
- sources ?: SourceData
123
- backend ?: string
124
- citationComponent ?: ComponentType < CitationComponentProps >
125
- className ?: string
126
- languageRenderers ?: Record < string , ComponentType < LanguageRendererProps > >
127
- } ) {
144
+ } : MarkdownProps ) {
128
145
const processedContent = preprocessContent ( content )
129
146
130
147
return (
@@ -137,49 +154,53 @@ export function Markdown({
137
154
remarkPlugins = { [ remarkGfm , remarkMath ] }
138
155
rehypePlugins = { [ rehypeKatex as any ] }
139
156
components = { {
140
- p ( { children } ) {
157
+ ...components ,
158
+ p : combineComponent ( components ?. p , ( { children } ) => {
141
159
return < div className = "mb-2 last:mb-0" > { children } </ div >
142
- } ,
143
- code ( { inline, className, children, ...props } ) {
144
- if ( children . length ) {
145
- if ( children [ 0 ] === '▍' ) {
160
+ } ) ,
161
+ code : combineComponent (
162
+ components ?. code ,
163
+ ( { inline, className, children, ...props } ) => {
164
+ if ( children . length ) {
165
+ if ( children [ 0 ] === '▍' ) {
166
+ return (
167
+ < span className = "mt-1 animate-pulse cursor-default" > ▍</ span >
168
+ )
169
+ }
170
+
171
+ children [ 0 ] = ( children [ 0 ] as string ) . replace ( '`▍`' , '▍' )
172
+ }
173
+
174
+ const match = / l a n g u a g e - ( \w + ) / . exec ( className || '' )
175
+ const language = ( match && match [ 1 ] ) || ''
176
+ const codeValue = String ( children ) . replace ( / \n $ / , '' )
177
+
178
+ if ( inline ) {
146
179
return (
147
- < span className = "mt-1 animate-pulse cursor-default" > ▍</ span >
180
+ < code className = { className } { ...props } >
181
+ { children }
182
+ </ code >
148
183
)
149
184
}
150
185
151
- children [ 0 ] = ( children [ 0 ] as string ) . replace ( '`▍`' , '▍' )
152
- }
153
-
154
- const match = / l a n g u a g e - ( \w + ) / . exec ( className || '' )
155
- const language = ( match && match [ 1 ] ) || ''
156
- const codeValue = String ( children ) . replace ( / \n $ / , '' )
186
+ // Check for custom language renderer
187
+ if ( languageRenderers ?. [ language ] ) {
188
+ const CustomRenderer = languageRenderers [ language ]
189
+ return < CustomRenderer code = { codeValue } className = "mb-2" />
190
+ }
157
191
158
- if ( inline ) {
159
192
return (
160
- < code className = { className } { ...props } >
161
- { children }
162
- </ code >
193
+ < CodeBlock
194
+ key = { Math . random ( ) }
195
+ language = { language }
196
+ value = { codeValue }
197
+ className = "mb-2"
198
+ { ...props }
199
+ />
163
200
)
164
201
}
165
-
166
- // Check for custom language renderer
167
- if ( languageRenderers ?. [ language ] ) {
168
- const CustomRenderer = languageRenderers [ language ]
169
- return < CustomRenderer code = { codeValue } className = "mb-2" />
170
- }
171
-
172
- return (
173
- < CodeBlock
174
- key = { Math . random ( ) }
175
- language = { language }
176
- value = { codeValue }
177
- className = "mb-2"
178
- { ...props }
179
- />
180
- )
181
- } ,
182
- a ( { href, children } ) {
202
+ ) ,
203
+ a : combineComponent ( components ?. a , ( { href, children } ) => {
183
204
// If href starts with `{backend}/api/files`, then it's a local document and we use DocumentInfo for rendering
184
205
if ( href ?. startsWith ( `${ backend } /api/files` ) ) {
185
206
// Check if the file is document file type
@@ -231,7 +252,7 @@ export function Markdown({
231
252
{ children }
232
253
</ a >
233
254
)
234
- } ,
255
+ } ) ,
235
256
} }
236
257
>
237
258
{ processedContent }
0 commit comments