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) => ( -

    - {children} -
- ), - 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 ( +
      + {children} +
    + ) + }, + 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}