feat(web): render assistant Markdown

This commit is contained in:
Yichen Jiang
2026-07-23 00:39:27 +08:00
parent c5f5b29588
commit 6f624c67c4
15 changed files with 712 additions and 14 deletions

View File

@@ -0,0 +1,64 @@
import ReactMarkdown from 'react-markdown'
import type { Components, UrlTransform } from 'react-markdown'
import remarkGfm from 'remark-gfm'
import css from './MarkdownText.module.css'
const remarkPlugins = [remarkGfm]
function sanitizeUrl(url: string): string {
try {
switch (new URL(url).protocol) {
case 'http:':
case 'https:':
case 'mailto:':
return url
default:
return ''
}
} catch {
return ''
}
}
const safeUrl: UrlTransform = url => sanitizeUrl(url)
const components: Components = {
a: ({ href = '', children }) => {
const safeHref = sanitizeUrl(href)
if (safeHref === '') return <>{children}</>
const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
return (
<a
href={safeHref}
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
>
{children}
</a>
)
},
img: ({ alt = '' }) => <span className={css.imageAlt}>{alt}</span>,
table: ({ children }) => (
<div className={css.tableScroll}>
<table>{children}</table>
</div>
),
}
/**
* Render untrusted assistant-authored Markdown as semantic React elements.
* @param props - Markdown source text preserved by the session projection.
* @returns A GFM document with raw HTML, relative links, unsafe protocols, and remote images disabled.
*/
export function MarkdownText({ text }: { text: string }) {
return (
<div className={css.markdown}>
<ReactMarkdown
remarkPlugins={remarkPlugins}
components={components}
urlTransform={safeUrl}
>
{text}
</ReactMarkdown>
</div>
)
}