diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx
index 10be5a87703..45f4bc223ae 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx
@@ -290,6 +290,16 @@ function TextEditor({
}
}, [isResizing])
+ const handleCheckboxToggle = useCallback(
+ (checkboxIndex: number, checked: boolean) => {
+ const toggled = toggleMarkdownCheckbox(contentRef.current, checkboxIndex, checked)
+ if (toggled !== contentRef.current) {
+ handleContentChange(toggled)
+ }
+ },
+ [handleContentChange]
+ )
+
const isStreaming = streamingContent !== undefined
const revealedContent = useStreamingText(content, isStreaming)
@@ -392,10 +402,11 @@ function TextEditor({
className={cn('min-w-0 flex-1 overflow-hidden', isResizing && 'pointer-events-none')}
>
>
@@ -703,6 +714,14 @@ function PptxPreview({
)
}
+function toggleMarkdownCheckbox(markdown: string, targetIndex: number, checked: boolean): string {
+ let currentIndex = 0
+ return markdown.replace(/^(\s*(?:[-*+]|\d+[.)]) +)\[([ xX])\]/gm, (match, prefix: string) => {
+ if (currentIndex++ !== targetIndex) return match
+ return `${prefix}[${checked ? 'x' : ' '}]`
+ })
+}
+
const UnsupportedPreview = memo(function UnsupportedPreview({
file,
}: {
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx
index b3c9666d768..06678e6acd3 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx
@@ -1,9 +1,10 @@
'use client'
-import { memo, useMemo } from 'react'
+import { memo, useMemo, useRef } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkBreaks from 'remark-breaks'
import remarkGfm from 'remark-gfm'
+import { Checkbox } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
import { useAutoScroll } from '@/hooks/use-auto-scroll'
@@ -40,6 +41,7 @@ interface PreviewPanelProps {
mimeType: string | null
filename: string
isStreaming?: boolean
+ onCheckboxToggle?: (checkboxIndex: number, checked: boolean) => void
}
export const PreviewPanel = memo(function PreviewPanel({
@@ -47,11 +49,18 @@ export const PreviewPanel = memo(function PreviewPanel({
mimeType,
filename,
isStreaming,
+ onCheckboxToggle,
}: PreviewPanelProps) {
const previewType = resolvePreviewType(mimeType, filename)
if (previewType === 'markdown')
- return
+ return (
+
+ )
if (previewType === 'html') return
if (previewType === 'csv') return
if (previewType === 'svg') return
@@ -61,7 +70,7 @@ export const PreviewPanel = memo(function PreviewPanel({
const REMARK_PLUGINS = [remarkGfm, remarkBreaks]
-const PREVIEW_MARKDOWN_COMPONENTS = {
+const STATIC_MARKDOWN_COMPONENTS = {
p: ({ children }: any) => (
{children}
@@ -87,17 +96,6 @@ const PREVIEW_MARKDOWN_COMPONENTS = {
{children}
),
- ul: ({ children }: any) => (
-
- ),
- ol: ({ children }: any) => (
-
- {children}
-
- ),
- li: ({ children }: any) => {children},
code: ({ inline, className, children, ...props }: any) => {
const isInline = inline || !className?.includes('language-')
@@ -165,26 +163,110 @@ const PREVIEW_MARKDOWN_COMPONENTS = {
td: ({ children }: any) => {children} | ,
}
+function buildMarkdownComponents(
+ checkboxCounterRef: React.MutableRefObject,
+ onCheckboxToggle?: (checkboxIndex: number, checked: boolean) => void
+) {
+ const isInteractive = Boolean(onCheckboxToggle)
+
+ return {
+ ...STATIC_MARKDOWN_COMPONENTS,
+ ul: ({ className, children }: any) => {
+ const isTaskList = typeof className === 'string' && className.includes('contains-task-list')
+ return (
+
+ )
+ },
+ ol: ({ className, children }: any) => {
+ const isTaskList = typeof className === 'string' && className.includes('contains-task-list')
+ return (
+
+ {children}
+
+ )
+ },
+ li: ({ className, children }: any) => {
+ const isTaskItem = typeof className === 'string' && className.includes('task-list-item')
+ if (isTaskItem) {
+ return {children}
+ }
+ return {children}
+ },
+ input: ({ type, checked, ...props }: any) => {
+ if (type !== 'checkbox') return
+
+ const index = checkboxCounterRef.current++
+
+ return (
+ onCheckboxToggle!(index, Boolean(newChecked))
+ : undefined
+ }
+ disabled={!isInteractive}
+ size='sm'
+ className='mt-1 shrink-0'
+ />
+ )
+ },
+ }
+}
+
const MarkdownPreview = memo(function MarkdownPreview({
content,
isStreaming = false,
+ onCheckboxToggle,
}: {
content: string
isStreaming?: boolean
+ onCheckboxToggle?: (checkboxIndex: number, checked: boolean) => void
}) {
const { ref: scrollRef } = useAutoScroll(isStreaming)
const { committed, incoming, generation } = useStreamingReveal(content, isStreaming)
+ const checkboxCounterRef = useRef(0)
+
+ const components = useMemo(
+ () => buildMarkdownComponents(checkboxCounterRef, onCheckboxToggle),
+ [onCheckboxToggle]
+ )
+
+ checkboxCounterRef.current = 0
+
const committedMarkdown = useMemo(
() =>
committed ? (
-
+
{committed}
) : null,
- [committed]
+ [committed, components]
)
+ if (onCheckboxToggle) {
+ return (
+
+
+ {content}
+
+
+ )
+ }
+
return (
{committedMarkdown}
@@ -193,7 +275,7 @@ const MarkdownPreview = memo(function MarkdownPreview({
key={generation}
className={cn(isStreaming && 'animate-stream-fade-in', '[&>:first-child]:mt-0')}
>
-
+
{incoming}