diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/AnimationContext.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/AnimationContext.tsx new file mode 100644 index 000000000..7dbaaa411 --- /dev/null +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/AnimationContext.tsx @@ -0,0 +1,27 @@ +import React, { createContext, useContext } from 'react'; + +interface AnimationContextValue { + shouldStopAnimations: boolean; +} + +const AnimationContext = createContext(undefined); + +export const AnimationProvider: React.FC<{ + children: React.ReactNode; + shouldStopAnimations: boolean; +}> = ({ children, shouldStopAnimations }) => { + return ( + + {children} + + ); +}; + +export const useAnimationContext = () => { + const context = useContext(AnimationContext); + if (!context) { + // Return default value if context is not provided + return { shouldStopAnimations: false }; + } + return context; +}; diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/MarkdownComponent.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/MarkdownComponent.tsx index a9413fade..9bbbb75a6 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/MarkdownComponent.tsx +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/MarkdownComponent.tsx @@ -4,15 +4,7 @@ import { animateTokenizedText, createAnimationStyle } from './animation-helpers' import { cva } from 'class-variance-authority'; import { StreamingMessageCode } from '../../../streaming/StreamingMessageCode'; import { cn } from '@/lib/classMerge'; - -// Create a context to track list item state -const ListItemContext = React.createContext<{ - isInListItem: boolean; - initialHadParagraph: boolean | null; -}>({ - isInListItem: false, - initialHadParagraph: null -}); +import { useAnimationContext, AnimationProvider } from './AnimationContext'; type MarkdownComponentProps = { children: React.ReactNode; @@ -35,17 +27,14 @@ export const ParagraphComponent: React.FC = ({ style, ...rest }) => { - const listItemContext = React.useContext(ListItemContext); - - // If we're in a list item that started without paragraphs, don't render the p tag - if (listItemContext.isInListItem && listItemContext.initialHadParagraph === false) { - // Return children directly without the p tag wrapper - return <>{animateTokenizedText(children, rest)}; - } + const { shouldStopAnimations } = useAnimationContext(); return (

- {animateTokenizedText(children, rest)} + {animateTokenizedText(children, { + ...rest, + animation: shouldStopAnimations ? 'none' : rest.animation + })}

); }; @@ -263,21 +252,36 @@ export const ListItemComponent: React.FC = ({ style, ...rest }) => { - return ( -
  • span]:inline [&>span]:align-top', - '[&>p]:inline [&>p]:align-top' - )}> - {animateTokenizedText(children, rest)} -
  • + const numberOfChildrenIsLessThanPrevious = numberOfChildren < previousChildrenLength.current; + previousChildrenLength.current = numberOfChildren; + + if (numberOfChildrenIsLessThanPrevious) { + stopAnimations.current = true; + } + + return ( + +
  • span]:inline [&>span]:align-top', + '[&>p]:inline [&>p]:align-top' + )}> + {animateTokenizedText(children, { + ...rest, + animation: stopAnimations.current ? 'none' : rest.animation + })} +
  • +
    ); }; diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/TokenizedText.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/TokenizedText.tsx index 2f78c47fe..c8af97be4 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/TokenizedText.tsx +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/TokenizedText.tsx @@ -45,7 +45,7 @@ const TokenizedText: React.FC = React.memo( {chunk} ), - [animationStyle] + [animationStyle, doNotAnimateInitialText] ); const content = useMemo(() => { diff --git a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/animation-helpers.tsx b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/animation-helpers.tsx index 2ce6d88f2..1a81256a2 100644 --- a/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/animation-helpers.tsx +++ b/apps/web/src/components/ui/typography/AppMarkdownStreaming/AnimatedMarkdown/animation-helpers.tsx @@ -16,7 +16,8 @@ export type MarkdownAnimation = | 'typewriter' | 'highlight' | 'blurAndSharpen' - | 'dropIn'; + | 'dropIn' + | 'none'; export type MarkdownAnimationTimingFunction = 'ease-in-out' | 'ease-out' | 'ease-in' | 'linear'; @@ -34,7 +35,8 @@ export const animations: Record = { typewriter: 'buster-typewriter', highlight: 'buster-highlight', blurAndSharpen: 'buster-blurAndSharpen', - dropIn: 'buster-dropIn' + dropIn: 'buster-dropIn', + none: 'none' }; interface AnimationStyleProps { @@ -50,11 +52,12 @@ export const createAnimationStyle = ({ animationTimingFunction = 'ease-in-out', isStreamFinished = true }: AnimationStyleProps) => { + if (animation === 'none' || isStreamFinished) { + return { animation: 'none' }; + } + return { - animation: - animation && !isStreamFinished - ? `${animations[animation]} ${animationDuration}ms ${animationTimingFunction}` - : 'none' + animation: `${animations[animation]} ${animationDuration}ms ${animationTimingFunction}` }; }; @@ -115,7 +118,6 @@ export const animateTokenizedText = ( data-testid="other-markdown-element" style={{ ...createAnimationStyle(animationsProps), - animationIterationCount: 1, whiteSpace: 'pre-wrap', display: isInlineElement ? 'inline' : 'inline-block' }}> @@ -129,7 +131,6 @@ export const animateTokenizedText = ( data-testid="animated-markdown-element" style={{ ...createAnimationStyle(animationsProps), - animationIterationCount: 1, whiteSpace: 'pre-wrap', display: 'inline' }}>