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,6 @@ 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- ) ,
95- ol : ( { children } : any ) => (
96- < ol className = 'mt-1 mb-3 list-decimal space-y-1 break-words pl-6 text-[14px] text-[var(--text-primary)]' >
97- { children }
98- </ ol >
99- ) ,
100- li : ( { children } : any ) => < li className = 'break-words leading-[1.6]' > { children } </ li > ,
10199 code : ( { inline, className, children, ...props } : any ) => {
102100 const isInline = inline || ! className ?. includes ( 'language-' )
103101
@@ -165,26 +163,110 @@ const PREVIEW_MARKDOWN_COMPONENTS = {
165163 td : ( { children } : any ) => < td className = 'px-3 py-2 text-[var(--text-secondary)]' > { children } </ td > ,
166164}
167165
166+ function buildMarkdownComponents (
167+ checkboxCounterRef : React . MutableRefObject < number > ,
168+ onCheckboxToggle ?: ( checkboxIndex : number , checked : boolean ) => void
169+ ) {
170+ const isInteractive = Boolean ( onCheckboxToggle )
171+
172+ return {
173+ ...STATIC_MARKDOWN_COMPONENTS ,
174+ ul : ( { className, children } : any ) => {
175+ const isTaskList = typeof className === 'string' && className . includes ( 'contains-task-list' )
176+ return (
177+ < ul
178+ className = { cn (
179+ 'mt-1 mb-3 space-y-1 break-words text-[14px] text-[var(--text-primary)]' ,
180+ isTaskList ? 'list-none pl-0' : 'list-disc pl-6'
181+ ) }
182+ >
183+ { children }
184+ </ ul >
185+ )
186+ } ,
187+ ol : ( { className, children } : any ) => {
188+ const isTaskList = typeof className === 'string' && className . includes ( 'contains-task-list' )
189+ return (
190+ < ol
191+ className = { cn (
192+ 'mt-1 mb-3 space-y-1 break-words text-[14px] text-[var(--text-primary)]' ,
193+ isTaskList ? 'list-none pl-0' : 'list-decimal pl-6'
194+ ) }
195+ >
196+ { children }
197+ </ ol >
198+ )
199+ } ,
200+ li : ( { className, children } : any ) => {
201+ const isTaskItem = typeof className === 'string' && className . includes ( 'task-list-item' )
202+ if ( isTaskItem ) {
203+ return < li className = 'flex items-start gap-2 break-words leading-[1.6]' > { children } </ li >
204+ }
205+ return < li className = 'break-words leading-[1.6]' > { children } </ li >
206+ } ,
207+ input : ( { type, checked, ...props } : any ) => {
208+ if ( type !== 'checkbox' ) return < input type = { type } checked = { checked } { ...props } />
209+
210+ const index = checkboxCounterRef . current ++
211+
212+ return (
213+ < Checkbox
214+ checked = { checked ?? false }
215+ onCheckedChange = {
216+ isInteractive
217+ ? ( newChecked ) => onCheckboxToggle ! ( index , Boolean ( newChecked ) )
218+ : undefined
219+ }
220+ disabled = { ! isInteractive }
221+ size = 'sm'
222+ className = 'mt-1 shrink-0'
223+ />
224+ )
225+ } ,
226+ }
227+ }
228+
168229const MarkdownPreview = memo ( function MarkdownPreview ( {
169230 content,
170231 isStreaming = false ,
232+ onCheckboxToggle,
171233} : {
172234 content : string
173235 isStreaming ?: boolean
236+ onCheckboxToggle ?: ( checkboxIndex : number , checked : boolean ) => void
174237} ) {
175238 const { ref : scrollRef } = useAutoScroll ( isStreaming )
176239 const { committed, incoming, generation } = useStreamingReveal ( content , isStreaming )
177240
241+ const checkboxCounterRef = useRef ( 0 )
242+
243+ const components = useMemo (
244+ ( ) => buildMarkdownComponents ( checkboxCounterRef , onCheckboxToggle ) ,
245+ [ onCheckboxToggle ]
246+ )
247+
248+ checkboxCounterRef . current = 0
249+
178250 const committedMarkdown = useMemo (
179251 ( ) =>
180252 committed ? (
181- < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { PREVIEW_MARKDOWN_COMPONENTS } >
253+ < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { components } >
182254 { committed }
183255 </ ReactMarkdown >
184256 ) : null ,
185- [ committed ]
257+ [ committed , components ]
186258 )
187259
260+ if ( onCheckboxToggle ) {
261+ return (
262+ < div ref = { scrollRef } className = 'h-full overflow-auto p-6' >
263+ < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { components } >
264+ { content }
265+ </ ReactMarkdown >
266+ </ div >
267+ )
268+ }
269+
188270 return (
189271 < div ref = { scrollRef } className = 'h-full overflow-auto p-6' >
190272 { committedMarkdown }
@@ -193,7 +275,7 @@ const MarkdownPreview = memo(function MarkdownPreview({
193275 key = { generation }
194276 className = { cn ( isStreaming && 'animate-stream-fade-in' , '[&>:first-child]:mt-0' ) }
195277 >
196- < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { PREVIEW_MARKDOWN_COMPONENTS } >
278+ < ReactMarkdown remarkPlugins = { REMARK_PLUGINS } components = { components } >
197279 { incoming }
198280 </ ReactMarkdown >
199281 </ div >
0 commit comments