Skip to content

Commit 11aea6b

Browse files
committed
feat(files): interactive markdown checkbox toggling in preview
1 parent e2be992 commit 11aea6b

File tree

2 files changed

+95
-12
lines changed

2 files changed

+95
-12
lines changed

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ function TextEditor({
290290
}
291291
}, [isResizing])
292292

293+
const handleCheckboxToggle = useCallback(
294+
(checkboxIndex: number, checked: boolean) => {
295+
const toggled = toggleMarkdownCheckbox(contentRef.current, checkboxIndex, checked)
296+
if (toggled !== contentRef.current) {
297+
handleContentChange(toggled)
298+
}
299+
},
300+
[handleContentChange]
301+
)
302+
293303
const isStreaming = streamingContent !== undefined
294304
const revealedContent = useStreamingText(content, isStreaming)
295305

@@ -396,6 +406,7 @@ function TextEditor({
396406
mimeType={file.type}
397407
filename={file.name}
398408
isStreaming={isStreaming}
409+
onCheckboxToggle={canEdit && !isStreaming ? handleCheckboxToggle : undefined}
399410
/>
400411
</div>
401412
</>
@@ -703,6 +714,14 @@ function PptxPreview({
703714
)
704715
}
705716

717+
function toggleMarkdownCheckbox(markdown: string, targetIndex: number, checked: boolean): string {
718+
let currentIndex = 0
719+
return markdown.replace(/^(\s*[-*+]\s+)\[([ xX])\]/gm, (match, prefix: string) => {
720+
if (currentIndex++ !== targetIndex) return match
721+
return `${prefix}[${checked ? 'x' : ' '}]`
722+
})
723+
}
724+
706725
const UnsupportedPreview = memo(function UnsupportedPreview({
707726
file,
708727
}: {

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client'
22

3-
import { memo, useMemo } from 'react'
3+
import { memo, useMemo, useRef } from 'react'
44
import ReactMarkdown from 'react-markdown'
55
import remarkBreaks from 'remark-breaks'
66
import remarkGfm from 'remark-gfm'
7+
import { Checkbox } from '@/components/emcn'
78
import { cn } from '@/lib/core/utils/cn'
89
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
910
import { 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

4547
export 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

6271
const 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+
168221
const 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

Comments
 (0)