From 36ff13696720f4c177a01100ace1643170d7362f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 10 Jun 2026 12:30:59 -0700 Subject: [PATCH] fix(file-preview): gate streaming animation to prevent file patch issue with scroll based re-render --- .../components/file-viewer/preview-panel.tsx | 38 +++++++++++++++++-- apps/sim/hooks/use-smooth-text.ts | 10 +++-- 2 files changed, 41 insertions(+), 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 23ee5f6b010..e9da592dac0 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 @@ -35,7 +35,7 @@ import { cn } from '@/lib/core/utils/cn' import { extractTextContent } from '@/lib/core/utils/react-node-text' import { getFileExtension } from '@/lib/uploads/utils/file-utils' import { useScrollAnchor } from '@/hooks/use-scroll-anchor' -import { useSmoothText } from '@/hooks/use-smooth-text' +import { RESUME_SKIP_THRESHOLD, useSmoothText } from '@/hooks/use-smooth-text' import { DataTable } from './data-table' import { PreviewLoadingFrame } from './preview-shared' import { ZoomablePreview } from './zoomable-preview' @@ -185,6 +185,10 @@ function remarkCallouts() { const REMARK_PLUGINS = [remarkGfm, remarkBreaks, remarkMermaid, remarkCallouts] const REHYPE_PLUGINS = [rehypeSlug] +const STREAMDOWN_ALLOWED_TAGS: Record = { + 'mermaid-diagram': ['definition'], +} + /** * Soft per-character fade for newly revealed markdown while streaming. Mirrors * the chat surface so a streamed file preview reveals with the same cadence; @@ -197,6 +201,33 @@ const STREAM_ANIMATION = { sep: 'char', } as const +/** + * Gates the per-character fade to streams that build the document from + * scratch. Enabling the fade over an already-rendered document, or during + * in-place rewrites (patch snapshots), replays it on text that is already + * visible, so the gate latches off for those sessions. + */ +function useStreamAnimationGate(content: string, isStreaming: boolean): boolean { + const prevIsStreamingRef = useRef(false) + const prevContentRef = useRef(content) + const animateRef = useRef(false) + + if (isStreaming !== prevIsStreamingRef.current) { + animateRef.current = isStreaming && content.length <= RESUME_SKIP_THRESHOLD + } else if ( + isStreaming && + animateRef.current && + content !== prevContentRef.current && + !content.startsWith(prevContentRef.current) + ) { + animateRef.current = false + } + prevIsStreamingRef.current = isStreaming + prevContentRef.current = content + + return isStreaming && animateRef.current +} + /** * Carries the contentRef and toggle handler from MarkdownPreview down to the * task-list renderers. Only present when the preview is interactive. @@ -876,6 +907,7 @@ const MarkdownPreview = memo(function MarkdownPreview({ // jumping per server chunk. `snapOnNonAppend` shows in-place rewrites (patch) // in full immediately so a diff never appears to retype from the top. const revealedContent = useSmoothText(content, isStreaming, { snapOnNonAppend: true }) + const shouldAnimateStream = useStreamAnimationGate(content, isStreaming) const { ref: autoScrollRef, spacerRef } = useScrollAnchor( isStreaming && !disableAutoScroll, revealedContent @@ -927,12 +959,12 @@ const MarkdownPreview = memo(function MarkdownPreview({ {frontMatterData && } {markdownContent} diff --git a/apps/sim/hooks/use-smooth-text.ts b/apps/sim/hooks/use-smooth-text.ts index 5ad10a63421..292a032007b 100644 --- a/apps/sim/hooks/use-smooth-text.ts +++ b/apps/sim/hooks/use-smooth-text.ts @@ -10,11 +10,13 @@ const MIN_STEP = 1 const MAX_STEP = 400 /** - * Content already longer than this at mount is assumed to be an in-progress - * resume (or restored history), so it is shown immediately rather than replayed - * from the first character. + * Content already longer than this when streaming begins is assumed to be + * pre-existing (an in-progress resume, restored history, or an in-place edit + * of an existing document), so it is shown immediately rather than replayed + * from the first character. Consumers gating reveal animations should use the + * same threshold so pacing and animation agree on what counts as "new". */ -const RESUME_SKIP_THRESHOLD = 60 +export const RESUME_SKIP_THRESHOLD = 60 interface SmoothTextOptions { /**