From 11aea6b57101ec4ca08cfbe225a1bff490599825 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 13:17:14 -0700 Subject: [PATCH 1/8] feat(files): interactive markdown checkbox toggling in preview --- .../components/file-viewer/file-viewer.tsx | 19 ++++ .../components/file-viewer/preview-panel.tsx | 88 ++++++++++++++++--- 2 files changed, 95 insertions(+), 12 deletions(-) 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..e1b046bc932 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) @@ -396,6 +406,7 @@ function TextEditor({ mimeType={file.type} filename={file.name} isStreaming={isStreaming} + onCheckboxToggle={canEdit && !isStreaming ? handleCheckboxToggle : undefined} /> @@ -703,6 +714,14 @@ function PptxPreview({ ) } +function toggleMarkdownCheckbox(markdown: string, targetIndex: number, checked: boolean): string { + let currentIndex = 0 + return markdown.replace(/^(\s*[-*+]\s+)\[([ 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..13b6fbe2fb6 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,11 @@ const PREVIEW_MARKDOWN_COMPONENTS = { {children} ), - ul: ({ children }: any) => ( -

    - {children} -
- ), ol: ({ children }: any) => (
    {children}
), - li: ({ children }: any) =>
  • {children}
  • , code: ({ inline, className, children, ...props }: any) => { const isInline = inline || !className?.includes('language-') @@ -165,24 +168,85 @@ 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 ( +
      + {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] ) return ( @@ -193,7 +257,7 @@ const MarkdownPreview = memo(function MarkdownPreview({ key={generation} className={cn(isStreaming && 'animate-stream-fade-in', '[&>:first-child]:mt-0')} > - + {incoming} From a7e9399eeaf75d3cb924749cb67692659b34dfa6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 13:23:00 -0700 Subject: [PATCH 2/8] fix(files): handle ordered-list checkboxes and fix index drift --- .../files/components/file-viewer/file-viewer.tsx | 11 +++++++---- .../files/components/file-viewer/preview-panel.tsx | 7 +++++++ 2 files changed, 14 insertions(+), 4 deletions(-) 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 e1b046bc932..42bd438f43c 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 @@ -716,10 +716,13 @@ function PptxPreview({ function toggleMarkdownCheckbox(markdown: string, targetIndex: number, checked: boolean): string { let currentIndex = 0 - return markdown.replace(/^(\s*[-*+]\s+)\[([ xX])\]/gm, (match, prefix: string) => { - if (currentIndex++ !== targetIndex) return match - return `${prefix}[${checked ? 'x' : ' '}]` - }) + return markdown.replace( + /^(\s*(?:[-*+]|\d+[.)]) +)\[([ xX])\]/gm, + (match, prefix: string) => { + if (currentIndex++ !== targetIndex) return match + return `${prefix}[${checked ? 'x' : ' '}]` + } + ) } const UnsupportedPreview = memo(function UnsupportedPreview({ 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 13b6fbe2fb6..6bb973c4be8 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 @@ -237,6 +237,11 @@ const MarkdownPreview = memo(function MarkdownPreview({ [onCheckboxToggle] ) + const committedCheckboxCount = useMemo( + () => (committed ? (committed.match(/^[ \t]*(?:[-*+]|\d+[.)]) +\[[ xX]\]/gm) ?? []).length : 0), + [committed] + ) + checkboxCounterRef.current = 0 const committedMarkdown = useMemo( @@ -249,6 +254,8 @@ const MarkdownPreview = memo(function MarkdownPreview({ [committed, components] ) + checkboxCounterRef.current = committedCheckboxCount + return (
    {committedMarkdown} From 853a2a7faeadd5cdfd8ed28bf7e88587704a2c5d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 13:26:09 -0700 Subject: [PATCH 3/8] lint --- .../files/components/file-viewer/file-viewer.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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 42bd438f43c..497af489915 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 @@ -716,13 +716,10 @@ 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' : ' '}]` - } - ) + return markdown.replace(/^(\s*(?:[-*+]|\d+[.)]) +)\[([ xX])\]/gm, (match, prefix: string) => { + if (currentIndex++ !== targetIndex) return match + return `${prefix}[${checked ? 'x' : ' '}]` + }) } const UnsupportedPreview = memo(function UnsupportedPreview({ From 5721f70f82105492d160963d236fb24eae57d879 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 13:32:38 -0700 Subject: [PATCH 4/8] fix(files): remove counter offset that prevented checkbox toggling --- .../files/components/file-viewer/preview-panel.tsx | 7 ------- 1 file changed, 7 deletions(-) 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 6bb973c4be8..13b6fbe2fb6 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 @@ -237,11 +237,6 @@ const MarkdownPreview = memo(function MarkdownPreview({ [onCheckboxToggle] ) - const committedCheckboxCount = useMemo( - () => (committed ? (committed.match(/^[ \t]*(?:[-*+]|\d+[.)]) +\[[ xX]\]/gm) ?? []).length : 0), - [committed] - ) - checkboxCounterRef.current = 0 const committedMarkdown = useMemo( @@ -254,8 +249,6 @@ const MarkdownPreview = memo(function MarkdownPreview({ [committed, components] ) - checkboxCounterRef.current = committedCheckboxCount - return (
    {committedMarkdown} From 5f7c61f0c90acd875830e41bad9eeab52b4cb373 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 13:44:17 -0700 Subject: [PATCH 5/8] fix(files): apply task-list styling to ordered lists too --- .../components/file-viewer/preview-panel.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) 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 13b6fbe2fb6..c14a22e39bb 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 @@ -96,11 +96,6 @@ const STATIC_MARKDOWN_COMPONENTS = { {children} ), - ol: ({ children }: any) => ( -
      - {children} -
    - ), code: ({ inline, className, children, ...props }: any) => { const isInline = inline || !className?.includes('language-') @@ -189,6 +184,19 @@ function buildMarkdownComponents( ) }, + 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) { From 5d7b4d953998a0bbaa6c41c640670aecf3d38fe7 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 13:59:47 -0700 Subject: [PATCH 6/8] fix(files): render single pass when interactive to avoid index drift --- .../files/components/file-viewer/preview-panel.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 c14a22e39bb..079f606d100 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 @@ -247,6 +247,16 @@ const MarkdownPreview = memo(function MarkdownPreview({ checkboxCounterRef.current = 0 + if (onCheckboxToggle) { + return ( +
    + + {content} + +
    + ) + } + const committedMarkdown = useMemo( () => committed ? ( From 342696df4efc2db0e505f67f1567a2864ded28e0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 14:00:37 -0700 Subject: [PATCH 7/8] fix(files): move useMemo above conditional return to fix Rules of Hooks --- .../components/file-viewer/preview-panel.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 079f606d100..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 @@ -247,16 +247,6 @@ const MarkdownPreview = memo(function MarkdownPreview({ checkboxCounterRef.current = 0 - if (onCheckboxToggle) { - return ( -
    - - {content} - -
    - ) - } - const committedMarkdown = useMemo( () => committed ? ( @@ -267,6 +257,16 @@ const MarkdownPreview = memo(function MarkdownPreview({ [committed, components] ) + if (onCheckboxToggle) { + return ( +
    + + {content} + +
    + ) + } + return (
    {committedMarkdown} From 51163c65f875c409f2562e3554c782bd5c7ead11 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 14:15:17 -0700 Subject: [PATCH 8/8] fix(files): pass content directly to preview when not streaming to avoid stale frame --- .../[workspaceId]/files/components/file-viewer/file-viewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 497af489915..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 @@ -402,7 +402,7 @@ function TextEditor({ className={cn('min-w-0 flex-1 overflow-hidden', isResizing && 'pointer-events-none')} >