Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 139 additions & 163 deletions apps/sim/app/chat/components/message/components/markdown-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { type HTMLAttributes, memo, type ReactNode, useMemo } from 'react'
import React, { type HTMLAttributes, memo, type ReactNode } from 'react'
import { Streamdown } from 'streamdown'
import 'streamdown/styles.css'
import { CopyCodeButton, Tooltip } from '@/components/emcn'
import { extractTextContent } from '@/lib/core/utils/react-node-text'

export function LinkWithPreview({ href, children }: { href: string; children: React.ReactNode }) {
function LinkWithPreview({ href, children }: { href: string; children: React.ReactNode }) {
return (
<Tooltip.Root delayDuration={300}>
<Tooltip.Trigger asChild>
Expand All @@ -24,175 +24,151 @@ export function LinkWithPreview({ href, children }: { href: string; children: Re
)
}

function createCustomComponents(LinkComponent: typeof LinkWithPreview) {
return {
p: ({ children }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p className='mb-1 font-sans text-base text-gray-800 leading-relaxed last:mb-0 dark:text-gray-200'>
{children}
</p>
),

h1: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h1 className='mt-10 mb-5 font-sans font-semibold text-2xl text-gray-900 dark:text-gray-100'>
{children}
</h1>
),
h2: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h2 className='mt-8 mb-4 font-sans font-semibold text-gray-900 text-xl dark:text-gray-100'>
{children}
</h2>
),
h3: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h3 className='mt-7 mb-3 font-sans font-semibold text-gray-900 text-lg dark:text-gray-100'>
{children}
</h3>
),
h4: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h4 className='mt-5 mb-2 font-sans font-semibold text-base text-gray-900 dark:text-gray-100'>
{children}
</h4>
),

ul: ({ children }: React.HTMLAttributes<HTMLUListElement>) => (
<ul
className='mt-1 mb-1 space-y-1 pl-6 font-sans text-gray-800 dark:text-gray-200'
style={{ listStyleType: 'disc' }}
>
{children}
</ul>
),
ol: ({ children }: React.HTMLAttributes<HTMLOListElement>) => (
<ol
className='mt-1 mb-1 space-y-1 pl-6 font-sans text-gray-800 dark:text-gray-200'
style={{ listStyleType: 'decimal' }}
>
{children}
</ol>
),
li: ({ children }: React.LiHTMLAttributes<HTMLLIElement>) => (
<li className='font-sans text-gray-800 dark:text-gray-200' style={{ display: 'list-item' }}>
{children}
</li>
),

pre: ({ children }: HTMLAttributes<HTMLPreElement>) => {
let codeProps: HTMLAttributes<HTMLElement> = {}
let codeContent: ReactNode = children

if (
React.isValidElement<{ className?: string; children?: ReactNode }>(children) &&
children.type === 'code'
) {
const childElement = children as React.ReactElement<{
className?: string
children?: ReactNode
}>
codeProps = { className: childElement.props.className }
codeContent = childElement.props.children
}
const COMPONENTS = {
p: ({ children }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p className='mb-1 font-sans text-base text-gray-800 leading-relaxed last:mb-0 dark:text-gray-200'>
{children}
</p>
),

h1: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h1 className='mt-10 mb-5 font-sans font-semibold text-2xl text-gray-900 dark:text-gray-100'>
{children}
</h1>
),
h2: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h2 className='mt-8 mb-4 font-sans font-semibold text-gray-900 text-xl dark:text-gray-100'>
{children}
</h2>
),
h3: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h3 className='mt-7 mb-3 font-sans font-semibold text-gray-900 text-lg dark:text-gray-100'>
{children}
</h3>
),
h4: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h4 className='mt-5 mb-2 font-sans font-semibold text-base text-gray-900 dark:text-gray-100'>
{children}
</h4>
),

ul: ({ children }: React.HTMLAttributes<HTMLUListElement>) => (
<ul
className='mt-1 mb-1 space-y-1 pl-6 font-sans text-gray-800 dark:text-gray-200'
style={{ listStyleType: 'disc' }}
>
{children}
</ul>
),
ol: ({ children }: React.HTMLAttributes<HTMLOListElement>) => (
<ol
className='mt-1 mb-1 space-y-1 pl-6 font-sans text-gray-800 dark:text-gray-200'
style={{ listStyleType: 'decimal' }}
>
{children}
</ol>
),
li: ({ children }: React.LiHTMLAttributes<HTMLLIElement>) => (
<li className='font-sans text-gray-800 dark:text-gray-200' style={{ display: 'list-item' }}>
{children}
</li>
),

pre: ({ children }: HTMLAttributes<HTMLPreElement>) => {
let codeProps: HTMLAttributes<HTMLElement> = {}
let codeContent: ReactNode = children

if (
React.isValidElement<{ className?: string; children?: ReactNode }>(children) &&
children.type === 'code'
) {
const childElement = children as React.ReactElement<{
className?: string
children?: ReactNode
}>
codeProps = { className: childElement.props.className }
codeContent = childElement.props.children
}

return (
<div className='my-6 rounded-md bg-gray-900 text-sm dark:bg-black'>
<div className='flex items-center justify-between border-gray-700 border-b px-4 py-1.5 dark:border-gray-800'>
<span className='font-sans text-gray-400 text-xs'>
{codeProps.className?.replace('language-', '') || 'code'}
</span>
<CopyCodeButton
code={extractTextContent(codeContent)}
className='text-gray-400 hover-hover:bg-gray-700 hover-hover:text-gray-200'
/>
</div>
<pre className='overflow-x-auto p-4 font-mono text-gray-200 dark:text-gray-100'>
{codeContent}
</pre>
return (
<div className='my-6 rounded-md bg-gray-900 text-sm dark:bg-black'>
<div className='flex items-center justify-between border-gray-700 border-b px-4 py-1.5 dark:border-gray-800'>
<span className='font-sans text-gray-400 text-xs'>
{codeProps.className?.replace('language-', '') || 'code'}
</span>
<CopyCodeButton
code={extractTextContent(codeContent)}
className='text-gray-400 hover-hover:bg-gray-700 hover-hover:text-gray-200'
/>
</div>
)
},

inlineCode: ({ children }: { children?: React.ReactNode }) => (
<code className='rounded bg-gray-200 px-1 py-0.5 font-mono text-gray-800 text-inherit dark:bg-gray-700 dark:text-gray-200'>
{children}
</code>
),

blockquote: ({ children }: React.HTMLAttributes<HTMLQuoteElement>) => (
<blockquote className='my-4 border-gray-300 border-l-4 py-1 pl-4 font-sans text-gray-700 italic dark:border-gray-600 dark:text-gray-300'>
{children}
</blockquote>
),

hr: () => <hr className='my-8 border-gray-500/[.07] border-t dark:border-gray-400/[.07]' />,

a: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
<LinkComponent href={href || '#'} {...props}>
{children}
</LinkComponent>
),

table: ({ children }: React.TableHTMLAttributes<HTMLTableElement>) => (
<div className='my-4 w-full overflow-x-auto'>
<table className='min-w-full table-auto border border-gray-300 font-sans text-sm dark:border-gray-700'>
{children}
</table>
<pre className='overflow-x-auto p-4 font-mono text-gray-200 dark:text-gray-100'>
{codeContent}
</pre>
</div>
),
thead: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<thead className='bg-gray-100 text-left dark:bg-gray-800'>{children}</thead>
),
tbody: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<tbody className='divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900'>
{children}
</tbody>
),
tr: ({ children }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr className='border-gray-200 border-b transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800/60'>
{children}
</tr>
),
th: ({ children }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
<th className='border-gray-300 border-r px-4 py-2 font-medium text-gray-700 last:border-r-0 dark:border-gray-700 dark:text-gray-300'>
)
},

inlineCode: ({ children }: { children?: React.ReactNode }) => (
<code className='rounded bg-gray-200 px-1 py-0.5 font-mono text-gray-800 text-inherit dark:bg-gray-700 dark:text-gray-200'>
{children}
</code>
),

blockquote: ({ children }: React.HTMLAttributes<HTMLQuoteElement>) => (
<blockquote className='my-4 border-gray-300 border-l-4 py-1 pl-4 font-sans text-gray-700 italic dark:border-gray-600 dark:text-gray-300'>
{children}
</blockquote>
),

hr: () => <hr className='my-8 border-gray-500/[.07] border-t dark:border-gray-400/[.07]' />,

a: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
<LinkWithPreview href={href || '#'} {...props}>
{children}
</LinkWithPreview>
),

table: ({ children }: React.TableHTMLAttributes<HTMLTableElement>) => (
<div className='my-4 w-full overflow-x-auto'>
<table className='min-w-full table-auto border border-gray-300 font-sans text-sm dark:border-gray-700'>
{children}
</th>
),
td: ({ children }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
<td className='break-words border-gray-300 border-r px-4 py-2 text-gray-800 last:border-r-0 dark:border-gray-700 dark:text-gray-200'>
{children}
</td>
),

img: ({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) => (
<img
src={src}
alt={alt || 'Image'}
className='my-3 h-auto max-w-full rounded-md'
{...props}
/>
),
}
</table>
</div>
),
thead: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<thead className='bg-gray-100 text-left dark:bg-gray-800'>{children}</thead>
),
tbody: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<tbody className='divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900'>
{children}
</tbody>
),
tr: ({ children }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr className='border-gray-200 border-b transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800/60'>
{children}
</tr>
),
th: ({ children }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
<th className='border-gray-300 border-r px-4 py-2 font-medium text-gray-700 last:border-r-0 dark:border-gray-700 dark:text-gray-300'>
{children}
</th>
),
td: ({ children }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
<td className='break-words border-gray-300 border-r px-4 py-2 text-gray-800 last:border-r-0 dark:border-gray-700 dark:text-gray-200'>
{children}
</td>
),

img: ({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) => (
<img src={src} alt={alt || 'Image'} className='my-3 h-auto max-w-full rounded-md' {...props} />
),
}

const DEFAULT_COMPONENTS = createCustomComponents(LinkWithPreview)

const MarkdownRenderer = memo(function MarkdownRenderer({
content,
customLinkComponent,
}: {
content: string
customLinkComponent?: typeof LinkWithPreview
}) {
const components = useMemo(() => {
if (!customLinkComponent) {
return DEFAULT_COMPONENTS
}
return createCustomComponents(customLinkComponent)
}, [customLinkComponent])

const processedContent = content.trim()

const MarkdownRenderer = memo(function MarkdownRenderer({ content }: { content: string }) {
return (
<div className='space-y-4 break-words font-sans text-[var(--landing-text)] text-base leading-relaxed'>
<Streamdown mode='static' components={components}>
{processedContent}
<Streamdown mode='static' components={COMPONENTS}>
{content.trim()}
</Streamdown>
</div>
)
Expand Down
8 changes: 1 addition & 7 deletions apps/sim/app/chat/components/message/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
ChatFileDownloadAll,
} from '@/app/chat/components/message/components/file-download'
import MarkdownRenderer from '@/app/chat/components/message/components/markdown-renderer'
import { useThrottledValue } from '@/hooks/use-throttled-value'

export interface ChatAttachment {
id: string
Expand Down Expand Up @@ -39,11 +38,6 @@ export interface ChatMessage {
files?: ChatFile[]
}

function EnhancedMarkdownRenderer({ content }: { content: string }) {
const throttled = useThrottledValue(content)
return <MarkdownRenderer content={throttled} />
}

export const ClientChatMessage = memo(
function ClientChatMessage({ message }: { message: ChatMessage }) {
const [isCopied, setIsCopied] = useState(false)
Expand Down Expand Up @@ -188,7 +182,7 @@ export const ClientChatMessage = memo(
{JSON.stringify(cleanTextContent, null, 2)}
</pre>
) : (
<EnhancedMarkdownRenderer content={cleanTextContent as string} />
<MarkdownRenderer content={cleanTextContent as string} />
)}
</div>
</div>
Expand Down
Loading
Loading