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 { 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<MarkdownComponentProps> = ({
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 (
<p style={style} className={className} data-testid="paragraph-component">
{animateTokenizedText(children, rest)}
{animateTokenizedText(children, {
...rest,
animation: shouldStopAnimations ? 'none' : rest.animation
})}
</p>
);
};
@ -263,21 +252,36 @@ export const ListItemComponent: React.FC<MarkdownComponentProps> = ({
style,
...rest
}) => {
return (
<li
style={style}
className={cn(
className,
const numberOfChildren = React.Children.count(children);
const stopAnimations = React.useRef(false);
const previousChildrenLength = React.useRef(numberOfChildren);
'[&_span]:inline',
// // Normal text flow
'whitespace-normal',
// Fix alignment of content
'[&>span]:inline [&>span]:align-top',
'[&>p]:inline [&>p]:align-top'
)}>
{animateTokenizedText(children, rest)}
</li>
const numberOfChildrenIsLessThanPrevious = numberOfChildren < previousChildrenLength.current;
previousChildrenLength.current = numberOfChildren;
if (numberOfChildrenIsLessThanPrevious) {
stopAnimations.current = true;
}
return (
<AnimationProvider shouldStopAnimations={stopAnimations.current}>
<li
style={style}
className={cn(
className,
'[&_span]:inline',
// // Normal text flow
'whitespace-normal',
// Fix alignment of content
'[&>span]:inline [&>span]:align-top',
'[&>p]:inline [&>p]:align-top'
)}>
{animateTokenizedText(children, {
...rest,
animation: stopAnimations.current ? 'none' : rest.animation
})}
</li>
</AnimationProvider>
);
};

View File

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

View File

@ -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<MarkdownAnimation, string> = {
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'
}}>