11'use client'
22
3- import { memo , useMemo } from 'react'
3+ import { memo , useMemo , useRef } from 'react'
44import ReactMarkdown from 'react-markdown'
55import remarkBreaks from 'remark-breaks'
66import remarkGfm from 'remark-gfm'
7+ import { Checkbox } from '@/components/emcn'
78import { cn } from '@/lib/core/utils/cn'
89import { getFileExtension } from '@/lib/uploads/utils/file-utils'
910import { useAutoScroll } from '@/hooks/use-auto-scroll'
@@ -40,18 +41,26 @@ interface PreviewPanelProps {
4041 mimeType : string | null
4142 filename : string
4243 isStreaming ?: boolean
44+ onCheckboxToggle ?: ( checkboxIndex : number , checked : boolean ) => void
4345}
4446
4547export const PreviewPanel = memo ( function PreviewPanel ( {
4648 content,
4749 mimeType,
4850 filename,
4951 isStreaming,
52+ onCheckboxToggle,
5053} : PreviewPanelProps ) {
5154 const previewType = resolvePreviewType ( mimeType , filename )
5255
5356 if ( previewType === 'markdown' )
54- return < MarkdownPreview content = { content } isStreaming = { isStreaming } />
57+ return (
58+ < MarkdownPreview
59+ content = { content }
60+ isStreaming = { isStreaming }
61+ onCheckboxToggle = { onCheckboxToggle }
62+ />
63+ )
5564 if ( previewType === 'html' ) return < HtmlPreview content = { content } />
5665 if ( previewType === 'csv' ) return < CsvPreview content = { content } />
5766 if ( previewType === 'svg' ) return < SvgPreview content = { content } />
@@ -61,7 +70,7 @@ export const PreviewPanel = memo(function PreviewPanel({
6170
6271const REMARK_PLUGINS = [ remarkGfm , remarkBreaks ]
6372
64- const PREVIEW_MARKDOWN_COMPONENTS = {
73+ const STATIC_MARKDOWN_COMPONENTS = {
6574 p : ( { children } : any ) => (
6675 < p className = 'mb-3 break-words text-[14px] text-[var(--text-primary)] leading-[1.6] last:mb-0' >
6776 { children }
@@ -87,17 +96,11 @@ const PREVIEW_MARKDOWN_COMPONENTS = {
8796 { children }
8897 </ h4 >
8998 ) ,
90- ul : ( { children } : any ) => (
91- < ul className = 'mt-1 mb-3 list-disc space-y-1 break-words pl-6 text-[14px] text-[var(--text-primary)]' >
92- { children }
93- </ ul >
94- ) ,
9599 ol : ( { children } : any ) => (
96100 < ol className = 'mt-1 mb-3 list-decimal space-y-1 break-words pl-6 text-[14px] text-[var(--text-primary)]' >
97101 { children }
98102 </ ol >
99103 ) ,
100- li : ( { children } : any ) => < li className = 'break-words leading-[1.6]' > { children } </ li > ,
101104 code : ( { inline, className, children, ...props } : any ) => {
102105 const isInline = inline || ! className ?. includes ( 'language-' )
103106
@@ -165,24 +168,85 @@ const PREVIEW_MARKDOWN_COMPONENTS = {
165168 td : ( { children } : any ) => < td className = 'px-3 py-2 text-[var(--text-secondary)]' > { children } </ td > ,
166169}
167170
171+ function buildMarkdownComponents (
172+ checkboxCounterRef : React . MutableRefObject < number > ,
173+ onCheckboxToggle ?: ( checkboxIndex : number , checked : boolean ) => void
174+ ) {
175+ const isInteractive = Boolean ( onCheckboxToggle )
176+
177+ return {
178+ ...STATIC_MARKDOWN_COMPONENTS ,
179+ ul : ( { className, children } : any ) => {
180+ const isTaskList = typeof className === 'string' && className . includes ( 'contains-task-list' )
181+ return (
182+ < ul
183+ className = { cn (
184+ 'mt-1 mb-3 space-y-1 break-words text-[14px] text-[var(--text-primary)]' ,
185+ isTaskList ? 'list-none pl-0' : 'list-disc pl-6'
186+ ) }
187+ >
188+ { children }
189+ </ ul >
190+ )
191+ } ,
192+ li : ( { className, children } : any ) => {
193+ const isTaskItem = typeof className === 'string' && className . includes ( 'task-list-item' )
194+ if ( isTaskItem ) {
195+ return < li className = 'flex items-start gap-2 break-words leading-[1.6]' > { children } </ li >
196+ }
197+ return < li className = 'break-words leading-[1.6]' > { children } </ li >
198+ } ,
199+ input : ( { type, checked, ...props } : any ) => {
200+ if ( type !== 'checkbox' ) return < input type = { type } checked = { checked } { ...props } />
201+
202+ const index = checkboxCounterRef . current ++
203+
204+ return (
205+ < Checkbox
206+ checked = { checked ?? false }
207+ onCheckedChange = {
208+ isInteractive
209+ ? ( newChecked ) => onCheckboxToggle ! ( index , Boolean ( newChecked ) )
210+ : undefined
211+ }
212+ disabled = { ! isInteractive }
213+ size = 'sm'
214+ className = 'mt-1 shrink-0'
215+ />
216+ )
217+ } ,
218+ }
219+ }
220+
168221const MarkdownPreview = memo ( function MarkdownPreview ( {
169222 content,
170223 isStreaming = false ,
224+ onCheckboxToggle,
171225} : {
172226 content : string
173227 isStreaming ?: boolean
228+ onCheckboxToggle ?: ( checkboxIndex : number , checked : boolean ) => void
174229} ) {
175230 const { ref : scrollRef } = useAutoScroll ( isStreaming )
176231 const { committed, incoming, generation } = useStreamingReveal ( content , isStreaming )
177232
233+ const checkboxCounterRef = useRef ( 0 )
234+
235+ const components = useMemo (
236+ ( ) => buildMarkdownComponents ( checkboxCounterRef , onCheckboxToggle ) ,
237+ [ onCheckboxToggle ]
238+ )
239+
240+ checkboxCounterRef . current = 0
241+
178242 const committedMarkdown = useMemo (
179243 ( ) =>
180244 committed ? (
181- < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { PREVIEW_MARKDOWN_COMPONENTS } >
245+ < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { components } >
182246 { committed }
183247 </ ReactMarkdown >
184248 ) : null ,
185- [ committed ]
249+ [ committed , components ]
186250 )
187251
188252 return (
@@ -193,7 +257,7 @@ const MarkdownPreview = memo(function MarkdownPreview({
193257 key = { generation }
194258 className = { cn ( isStreaming && 'animate-stream-fade-in' , '[&>:first-child]:mt-0' ) }
195259 >
196- < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { PREVIEW_MARKDOWN_COMPONENTS } >
260+ < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { components } >
197261 { incoming }
198262 </ ReactMarkdown >
199263 </ div >
0 commit comments