Skip to content

Commit b092d5e

Browse files
pmn4thucpn
andauthored
Expose ReactMarkdown components prop (#193)
* Expose ReactMarkdown `components` prop * Expose ReactMarkdown components prop --------- Co-authored-by: Thuc Pham <[email protected]>
1 parent e09ac1c commit b092d5e

File tree

3 files changed

+80
-54
lines changed

3 files changed

+80
-54
lines changed

.changeset/curly-buses-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@llamaindex/chat-ui": patch
3+
---
4+
5+
Expose ReactMarkdown `components` prop

packages/chat-ui/src/chat/message-parts/parts/markdown.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
import { ComponentType } from 'react'
21
import { cn } from '../../../lib/utils.js'
32
import {
4-
CitationComponentProps,
5-
LanguageRendererProps,
63
Markdown,
74
preprocessSourceNodes,
5+
type MarkdownProps,
86
} from '../../../widgets/index.js'
97
import { useChatMessage } from '../../chat-message.context.js'
10-
import { SourcesPartType, TextPartType } from '../types.js'
118
import { usePart } from '../context.js'
9+
import { SourcesPartType, TextPartType } from '../types.js'
1210
import { getParts } from '../utils.js'
1311

1412
interface ChatMarkdownProps extends React.PropsWithChildren {
15-
citationComponent?: ComponentType<CitationComponentProps>
13+
components?: MarkdownProps['components']
14+
citationComponent?: MarkdownProps['citationComponent']
1615
className?: string
17-
languageRenderers?: Record<string, ComponentType<LanguageRendererProps>>
16+
languageRenderers?: MarkdownProps['languageRenderers']
1817
}
1918

2019
/**
@@ -41,6 +40,7 @@ export function MarkdownPartUI(props: ChatMarkdownProps) {
4140
<Markdown
4241
content={markdown}
4342
sources={{ nodes }}
43+
components={props.components}
4444
citationComponent={props.citationComponent}
4545
languageRenderers={props.languageRenderers}
4646
className={cn(

packages/chat-ui/src/widgets/markdown.tsx

Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
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'
33
import rehypeKatex from 'rehype-katex'
44
import remarkGfm from 'remark-gfm'
55
import remarkMath from 'remark-math'
6-
import { CodeBlock } from './codeblock'
7-
import { DocumentInfo } from './document-info'
6+
import { cn } from '../lib/utils'
87
import { SourceData } from './chat-sources'
98
import { Citation, CitationComponentProps } from './citation'
10-
import { cn } from '../lib/utils'
9+
import { CodeBlock } from './codeblock'
10+
import { DocumentInfo } from './document-info'
1111

1212
const MemoizedReactMarkdown: FC<Options> = memo(
1313
ReactMarkdown,
@@ -110,21 +110,38 @@ export interface LanguageRendererProps {
110110
className?: string
111111
}
112112

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+
}
113136
export function Markdown({
114137
content,
115138
sources,
116139
backend,
117140
citationComponent: CitationComponent,
118141
className: customClassName,
142+
components,
119143
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) {
128145
const processedContent = preprocessContent(content)
129146

130147
return (
@@ -137,49 +154,53 @@ export function Markdown({
137154
remarkPlugins={[remarkGfm, remarkMath]}
138155
rehypePlugins={[rehypeKatex as any]}
139156
components={{
140-
p({ children }) {
157+
...components,
158+
p: combineComponent(components?.p, ({ children }) => {
141159
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 = /language-(\w+)/.exec(className || '')
175+
const language = (match && match[1]) || ''
176+
const codeValue = String(children).replace(/\n$/, '')
177+
178+
if (inline) {
146179
return (
147-
<span className="mt-1 animate-pulse cursor-default"></span>
180+
<code className={className} {...props}>
181+
{children}
182+
</code>
148183
)
149184
}
150185

151-
children[0] = (children[0] as string).replace('`▍`', '▍')
152-
}
153-
154-
const match = /language-(\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+
}
157191

158-
if (inline) {
159192
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+
/>
163200
)
164201
}
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 }) => {
183204
// If href starts with `{backend}/api/files`, then it's a local document and we use DocumentInfo for rendering
184205
if (href?.startsWith(`${backend}/api/files`)) {
185206
// Check if the file is document file type
@@ -231,7 +252,7 @@ export function Markdown({
231252
{children}
232253
</a>
233254
)
234-
},
255+
}),
235256
}}
236257
>
237258
{processedContent}

0 commit comments

Comments
 (0)