11'use client'
22
33import { type ComponentPropsWithoutRef , useMemo } from 'react'
4- import { code } from '@streamdown/code'
54import { Streamdown } from 'streamdown'
65import 'streamdown/styles.css'
7- import { Checkbox } from '@/components/emcn'
6+ import 'prismjs/components/prism-typescript'
7+ import 'prismjs/components/prism-bash'
8+ import 'prismjs/components/prism-css'
9+ import 'prismjs/components/prism-markup'
10+ import '@/components/emcn/components/code/code.css'
11+ import { Checkbox , highlight , languages } from '@/components/emcn'
12+ import { CopyCodeButton } from '@/components/ui/copy-code-button'
813import { cn } from '@/lib/core/utils/cn'
14+ import { extractTextContent } from '@/lib/core/utils/react-node-text'
915import {
1016 PendingTagIndicator ,
1117 parseSpecialTags ,
1218 SpecialTags ,
1319} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
1420import { useStreamingText } from '@/hooks/use-streaming-text'
1521
22+ const LANG_ALIASES : Record < string , string > = {
23+ js : 'javascript' ,
24+ ts : 'typescript' ,
25+ tsx : 'typescript' ,
26+ jsx : 'javascript' ,
27+ sh : 'bash' ,
28+ shell : 'bash' ,
29+ html : 'markup' ,
30+ xml : 'markup' ,
31+ yml : 'yaml' ,
32+ py : 'python' ,
33+ }
34+
1635const PROSE_CLASSES = cn (
1736 'prose prose-base dark:prose-invert max-w-none' ,
1837 'font-[family-name:var(--font-inter)] antialiased break-words font-[430] tracking-[0]' ,
@@ -24,17 +43,13 @@ const PROSE_CLASSES = cn(
2443 'prose-ul:my-4 prose-ol:my-4' ,
2544 'prose-strong:font-[600] prose-strong:text-[var(--text-primary)]' ,
2645 'prose-a:text-[var(--text-primary)] prose-a:underline prose-a:decoration-dashed prose-a:underline-offset-4' ,
27- 'prose-code:rounded prose-code:bg-[var(--surface-5)] prose-code:px-1.5 prose-code:py-0.5 prose-code:text-small prose-code:font-mono prose-code:font-[400] prose-code:text-[var(--text-primary)]' ,
28- 'prose-code:before:content-none prose-code:after:content-none' ,
2946 'prose-hr:border-[var(--divider)] prose-hr:my-6' ,
3047 'prose-table:my-0'
3148)
3249
3350type TdProps = ComponentPropsWithoutRef < 'td' >
3451type ThProps = ComponentPropsWithoutRef < 'th' >
3552
36- const STREAMDOWN_PLUGINS = { code }
37-
3853const MARKDOWN_COMPONENTS = {
3954 table ( { children } : { children ?: React . ReactNode } ) {
4055 return (
@@ -68,6 +83,41 @@ const MARKDOWN_COMPONENTS = {
6883 </ td >
6984 )
7085 } ,
86+ code ( { children, className } : { children ?: React . ReactNode ; className ?: string } ) {
87+ const langMatch = className ?. match ( / l a n g u a g e - ( \w + ) / )
88+ const language = langMatch ? langMatch [ 1 ] : ''
89+ const codeString = extractTextContent ( children )
90+
91+ if ( ! codeString ) {
92+ return (
93+ < pre className = 'not-prose my-6 overflow-x-auto rounded-lg bg-[var(--surface-5)] p-4 font-[430] font-mono text-[var(--text-primary)] text-small leading-[21px] dark:bg-[var(--code-bg)]' >
94+ < code > { children } </ code >
95+ </ pre >
96+ )
97+ }
98+
99+ const resolved = LANG_ALIASES [ language ] || language || 'javascript'
100+ const grammar = languages [ resolved ] || languages . javascript
101+ const html = highlight ( codeString . trimEnd ( ) , grammar , resolved )
102+
103+ return (
104+ < div className = 'not-prose my-6 overflow-hidden rounded-lg border border-[var(--divider)]' >
105+ < div className = 'flex items-center justify-between border-[var(--divider)] border-b bg-[var(--surface-4)] px-4 py-2 dark:bg-[var(--surface-4)]' >
106+ < span className = 'text-[var(--text-tertiary)] text-xs' > { language || 'code' } </ span >
107+ < CopyCodeButton
108+ code = { codeString }
109+ className = 'text-[var(--text-tertiary)] hover:bg-[var(--surface-5)] hover:text-[var(--text-secondary)]'
110+ />
111+ </ div >
112+ < div className = 'code-editor-theme bg-[var(--surface-5)] dark:bg-[var(--code-bg)]' >
113+ < pre
114+ className = 'm-0 overflow-x-auto whitespace-pre p-4 font-[430] font-mono text-[var(--text-primary)] text-small leading-[21px]'
115+ dangerouslySetInnerHTML = { { __html : html } }
116+ />
117+ </ div >
118+ </ div >
119+ )
120+ } ,
71121 a ( { children, href } : { children ?: React . ReactNode ; href ?: string } ) {
72122 return (
73123 < a
@@ -103,6 +153,13 @@ const MARKDOWN_COMPONENTS = {
103153 </ li >
104154 )
105155 } ,
156+ inlineCode ( { children } : { children ?: React . ReactNode } ) {
157+ return (
158+ < code className = 'rounded bg-[var(--surface-5)] px-1.5 py-0.5 font-mono text-small font-[400] text-[var(--text-primary)] before:content-none after:content-none' >
159+ { children }
160+ </ code >
161+ )
162+ } ,
106163 input ( { type, checked } : { type ?: string ; checked ?: boolean } ) {
107164 if ( type === 'checkbox' ) {
108165 return < Checkbox checked = { checked || false } disabled size = 'sm' className = 'mt-1.5 shrink-0' />
@@ -133,11 +190,7 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch
133190 key = { `${ segment . type } -${ i } ` }
134191 className = { cn ( PROSE_CLASSES , '[&>:first-child]:mt-0 [&>:last-child]:mb-0' ) }
135192 >
136- < Streamdown
137- mode = 'static'
138- plugins = { STREAMDOWN_PLUGINS }
139- components = { MARKDOWN_COMPONENTS }
140- >
193+ < Streamdown mode = 'static' components = { MARKDOWN_COMPONENTS } >
141194 { segment . content }
142195 </ Streamdown >
143196 </ div >
@@ -154,12 +207,7 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch
154207
155208 return (
156209 < div className = { cn ( PROSE_CLASSES , '[&>:first-child]:mt-0 [&>:last-child]:mb-0' ) } >
157- < Streamdown
158- isAnimating = { isStreaming }
159- animated
160- plugins = { STREAMDOWN_PLUGINS }
161- components = { MARKDOWN_COMPONENTS }
162- >
210+ < Streamdown isAnimating = { isStreaming } animated components = { MARKDOWN_COMPONENTS } >
163211 { rendered }
164212 </ Streamdown >
165213 </ div >
0 commit comments