Skip to content

Commit bc371b0

Browse files
fix(file-preview): gate streaming animation to prevent file patch issue with scroll based re-render (#4946)
1 parent 1ff445a commit bc371b0

2 files changed

Lines changed: 41 additions & 7 deletions

File tree

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { cn } from '@/lib/core/utils/cn'
3535
import { extractTextContent } from '@/lib/core/utils/react-node-text'
3636
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
3737
import { useScrollAnchor } from '@/hooks/use-scroll-anchor'
38-
import { useSmoothText } from '@/hooks/use-smooth-text'
38+
import { RESUME_SKIP_THRESHOLD, useSmoothText } from '@/hooks/use-smooth-text'
3939
import { DataTable } from './data-table'
4040
import { PreviewLoadingFrame } from './preview-shared'
4141
import { ZoomablePreview } from './zoomable-preview'
@@ -185,6 +185,10 @@ function remarkCallouts() {
185185
const REMARK_PLUGINS = [remarkGfm, remarkBreaks, remarkMermaid, remarkCallouts]
186186
const REHYPE_PLUGINS = [rehypeSlug]
187187

188+
const STREAMDOWN_ALLOWED_TAGS: Record<string, string[]> = {
189+
'mermaid-diagram': ['definition'],
190+
}
191+
188192
/**
189193
* Soft per-character fade for newly revealed markdown while streaming. Mirrors
190194
* the chat surface so a streamed file preview reveals with the same cadence;
@@ -197,6 +201,33 @@ const STREAM_ANIMATION = {
197201
sep: 'char',
198202
} as const
199203

204+
/**
205+
* Gates the per-character fade to streams that build the document from
206+
* scratch. Enabling the fade over an already-rendered document, or during
207+
* in-place rewrites (patch snapshots), replays it on text that is already
208+
* visible, so the gate latches off for those sessions.
209+
*/
210+
function useStreamAnimationGate(content: string, isStreaming: boolean): boolean {
211+
const prevIsStreamingRef = useRef(false)
212+
const prevContentRef = useRef(content)
213+
const animateRef = useRef(false)
214+
215+
if (isStreaming !== prevIsStreamingRef.current) {
216+
animateRef.current = isStreaming && content.length <= RESUME_SKIP_THRESHOLD
217+
} else if (
218+
isStreaming &&
219+
animateRef.current &&
220+
content !== prevContentRef.current &&
221+
!content.startsWith(prevContentRef.current)
222+
) {
223+
animateRef.current = false
224+
}
225+
prevIsStreamingRef.current = isStreaming
226+
prevContentRef.current = content
227+
228+
return isStreaming && animateRef.current
229+
}
230+
200231
/**
201232
* Carries the contentRef and toggle handler from MarkdownPreview down to the
202233
* task-list renderers. Only present when the preview is interactive.
@@ -876,6 +907,7 @@ const MarkdownPreview = memo(function MarkdownPreview({
876907
// jumping per server chunk. `snapOnNonAppend` shows in-place rewrites (patch)
877908
// in full immediately so a diff never appears to retype from the top.
878909
const revealedContent = useSmoothText(content, isStreaming, { snapOnNonAppend: true })
910+
const shouldAnimateStream = useStreamAnimationGate(content, isStreaming)
879911
const { ref: autoScrollRef, spacerRef } = useScrollAnchor(
880912
isStreaming && !disableAutoScroll,
881913
revealedContent
@@ -927,12 +959,12 @@ const MarkdownPreview = memo(function MarkdownPreview({
927959
{frontMatterData && <FrontMatterCard data={frontMatterData} />}
928960
<Streamdown
929961
mode={streamdownMode}
930-
animated={isStreaming ? STREAM_ANIMATION : false}
962+
animated={shouldAnimateStream ? STREAM_ANIMATION : false}
931963
isAnimating={isStreaming}
932964
remarkPlugins={REMARK_PLUGINS}
933965
rehypePlugins={REHYPE_PLUGINS}
934966
components={markdownComponents}
935-
allowedTags={{ 'mermaid-diagram': ['definition'] }}
967+
allowedTags={STREAMDOWN_ALLOWED_TAGS}
936968
>
937969
{markdownContent}
938970
</Streamdown>

apps/sim/hooks/use-smooth-text.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ const MIN_STEP = 1
1010
const MAX_STEP = 400
1111

1212
/**
13-
* Content already longer than this at mount is assumed to be an in-progress
14-
* resume (or restored history), so it is shown immediately rather than replayed
15-
* from the first character.
13+
* Content already longer than this when streaming begins is assumed to be
14+
* pre-existing (an in-progress resume, restored history, or an in-place edit
15+
* of an existing document), so it is shown immediately rather than replayed
16+
* from the first character. Consumers gating reveal animations should use the
17+
* same threshold so pacing and animation agree on what counts as "new".
1618
*/
17-
const RESUME_SKIP_THRESHOLD = 60
19+
export const RESUME_SKIP_THRESHOLD = 60
1820

1921
interface SmoothTextOptions {
2022
/**

0 commit comments

Comments
 (0)