update animation lifecycle

This commit is contained in:
Nate Kelley 2025-07-21 12:20:54 -06:00
parent d437535d50
commit a2ae1b8b38
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 72 additions and 40 deletions

View File

@ -0,0 +1,27 @@
import React, { createContext, useContext } from 'react';
interface AnimationContextValue {
shouldStopAnimations: boolean;
}
const AnimationContext = createContext<AnimationContextValue | undefined>(undefined);
export const AnimationProvider: React.FC<{
children: React.ReactNode;
shouldStopAnimations: boolean;
}> = ({ children, shouldStopAnimations }) => {
return (
<AnimationContext.Provider value={{ shouldStopAnimations }}>
{children}
</AnimationContext.Provider>
);
};
export const useAnimationContext = () => {
const context = useContext(AnimationContext);
if (!context) {
// Return default value if context is not provided
return { shouldStopAnimations: false };
}
return context;
};

View File

@ -4,15 +4,7 @@ import { animateTokenizedText, createAnimationStyle } from './animation-helpers'
import { cva } from 'class-variance-authority'; import { cva } from 'class-variance-authority';
import { StreamingMessageCode } from '../../../streaming/StreamingMessageCode'; import { StreamingMessageCode } from '../../../streaming/StreamingMessageCode';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
import { useAnimationContext, AnimationProvider } from './AnimationContext';
// Create a context to track list item state
const ListItemContext = React.createContext<{
isInListItem: boolean;
initialHadParagraph: boolean | null;
}>({
isInListItem: false,
initialHadParagraph: null
});
type MarkdownComponentProps = { type MarkdownComponentProps = {
children: React.ReactNode; children: React.ReactNode;
@ -35,17 +27,14 @@ export const ParagraphComponent: React.FC<MarkdownComponentProps> = ({
style, style,
...rest ...rest
}) => { }) => {
const listItemContext = React.useContext(ListItemContext); const { shouldStopAnimations } = useAnimationContext();
// 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)}</>;
}
return ( return (
<p style={style} className={className} data-testid="paragraph-component"> <p style={style} className={className} data-testid="paragraph-component">
{animateTokenizedText(children, rest)} {animateTokenizedText(children, {
...rest,
animation: shouldStopAnimations ? 'none' : rest.animation
})}
</p> </p>
); );
}; };
@ -263,12 +252,23 @@ export const ListItemComponent: React.FC<MarkdownComponentProps> = ({
style, style,
...rest ...rest
}) => { }) => {
const numberOfChildren = React.Children.count(children);
const stopAnimations = React.useRef(false);
const previousChildrenLength = React.useRef(numberOfChildren);
const numberOfChildrenIsLessThanPrevious = numberOfChildren < previousChildrenLength.current;
previousChildrenLength.current = numberOfChildren;
if (numberOfChildrenIsLessThanPrevious) {
stopAnimations.current = true;
}
return ( return (
<AnimationProvider shouldStopAnimations={stopAnimations.current}>
<li <li
style={style} style={style}
className={cn( className={cn(
className, className,
'[&_span]:inline', '[&_span]:inline',
// // Normal text flow // // Normal text flow
'whitespace-normal', 'whitespace-normal',
@ -276,8 +276,12 @@ export const ListItemComponent: React.FC<MarkdownComponentProps> = ({
'[&>span]:inline [&>span]:align-top', '[&>span]:inline [&>span]:align-top',
'[&>p]:inline [&>p]:align-top' '[&>p]:inline [&>p]:align-top'
)}> )}>
{animateTokenizedText(children, rest)} {animateTokenizedText(children, {
...rest,
animation: stopAnimations.current ? 'none' : rest.animation
})}
</li> </li>
</AnimationProvider>
); );
}; };

View File

@ -45,7 +45,7 @@ const TokenizedText: React.FC<TokenizedTextProps> = React.memo(
{chunk} {chunk}
</span> </span>
), ),
[animationStyle] [animationStyle, doNotAnimateInitialText]
); );
const content = useMemo(() => { const content = useMemo(() => {

View File

@ -16,7 +16,8 @@ export type MarkdownAnimation =
| 'typewriter' | 'typewriter'
| 'highlight' | 'highlight'
| 'blurAndSharpen' | 'blurAndSharpen'
| 'dropIn'; | 'dropIn'
| 'none';
export type MarkdownAnimationTimingFunction = 'ease-in-out' | 'ease-out' | 'ease-in' | 'linear'; export type MarkdownAnimationTimingFunction = 'ease-in-out' | 'ease-out' | 'ease-in' | 'linear';
@ -34,7 +35,8 @@ export const animations: Record<MarkdownAnimation, string> = {
typewriter: 'buster-typewriter', typewriter: 'buster-typewriter',
highlight: 'buster-highlight', highlight: 'buster-highlight',
blurAndSharpen: 'buster-blurAndSharpen', blurAndSharpen: 'buster-blurAndSharpen',
dropIn: 'buster-dropIn' dropIn: 'buster-dropIn',
none: 'none'
}; };
interface AnimationStyleProps { interface AnimationStyleProps {
@ -50,11 +52,12 @@ export const createAnimationStyle = ({
animationTimingFunction = 'ease-in-out', animationTimingFunction = 'ease-in-out',
isStreamFinished = true isStreamFinished = true
}: AnimationStyleProps) => { }: AnimationStyleProps) => {
if (animation === 'none' || isStreamFinished) {
return { animation: 'none' };
}
return { return {
animation: animation: `${animations[animation]} ${animationDuration}ms ${animationTimingFunction}`
animation && !isStreamFinished
? `${animations[animation]} ${animationDuration}ms ${animationTimingFunction}`
: 'none'
}; };
}; };
@ -115,7 +118,6 @@ export const animateTokenizedText = (
data-testid="other-markdown-element" data-testid="other-markdown-element"
style={{ style={{
...createAnimationStyle(animationsProps), ...createAnimationStyle(animationsProps),
animationIterationCount: 1,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
display: isInlineElement ? 'inline' : 'inline-block' display: isInlineElement ? 'inline' : 'inline-block'
}}> }}>
@ -129,7 +131,6 @@ export const animateTokenizedText = (
data-testid="animated-markdown-element" data-testid="animated-markdown-element"
style={{ style={{
...createAnimationStyle(animationsProps), ...createAnimationStyle(animationsProps),
animationIterationCount: 1,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
display: 'inline' display: 'inline'
}}> }}>