changes
This commit is contained in:
@@ -8,56 +8,56 @@ interface StreamingTextProps {
|
|||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TextChunk {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that renders text with a smooth fade-in animation for new chunks.
|
* Component that renders text with a smooth fade-in animation for new chunks.
|
||||||
* Splits content into "committed" (already visible) and "new" (fading in) portions.
|
* Each chunk gets its own span element with a fade-in animation.
|
||||||
*/
|
*/
|
||||||
export function StreamingText({ content, isStreaming }: StreamingTextProps) {
|
export function StreamingText({ content, isStreaming }: StreamingTextProps) {
|
||||||
const [committedLength, setCommittedLength] = useState(0);
|
const [chunks, setChunks] = useState<TextChunk[]>([]);
|
||||||
const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const prevLengthRef = useRef(0);
|
||||||
|
const chunkIdRef = useRef(0);
|
||||||
|
|
||||||
// When content grows, schedule the new content to become "committed" after animation
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (content.length > committedLength) {
|
const prevLength = prevLengthRef.current;
|
||||||
// Clear any pending timeout
|
const currentLength = content.length;
|
||||||
if (animationTimeoutRef.current) {
|
|
||||||
clearTimeout(animationTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// After the fade-in animation completes, commit the new content
|
if (currentLength > prevLength) {
|
||||||
animationTimeoutRef.current = setTimeout(() => {
|
// New content arrived - create a new chunk for it
|
||||||
setCommittedLength(content.length);
|
const newText = content.slice(prevLength);
|
||||||
}, 300); // Match CSS animation duration
|
const newChunk: TextChunk = {
|
||||||
}
|
id: chunkIdRef.current++,
|
||||||
|
text: newText,
|
||||||
return () => {
|
};
|
||||||
if (animationTimeoutRef.current) {
|
setChunks((prev) => [...prev, newChunk]);
|
||||||
clearTimeout(animationTimeoutRef.current);
|
prevLengthRef.current = currentLength;
|
||||||
}
|
} else if (currentLength < prevLength) {
|
||||||
};
|
// Content was reset (new message)
|
||||||
}, [content, committedLength]);
|
setChunks([]);
|
||||||
|
prevLengthRef.current = 0;
|
||||||
// When streaming stops, immediately commit all content
|
chunkIdRef.current = 0;
|
||||||
useEffect(() => {
|
|
||||||
if (!isStreaming) {
|
|
||||||
setCommittedLength(content.length);
|
|
||||||
}
|
|
||||||
}, [isStreaming, content.length]);
|
|
||||||
|
|
||||||
// Reset when content is cleared (new message)
|
|
||||||
useEffect(() => {
|
|
||||||
if (content.length === 0) {
|
|
||||||
setCommittedLength(0);
|
|
||||||
}
|
}
|
||||||
}, [content]);
|
}, [content]);
|
||||||
|
|
||||||
const committedText = content.slice(0, committedLength);
|
// When streaming stops, consolidate all chunks into one (removes animations)
|
||||||
const newText = content.slice(committedLength);
|
useEffect(() => {
|
||||||
|
if (!isStreaming && content.length > 0) {
|
||||||
|
setChunks([{ id: 0, text: content }]);
|
||||||
|
prevLengthRef.current = content.length;
|
||||||
|
}
|
||||||
|
}, [isStreaming, content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classes.streamingText}>
|
<span className={classes.streamingText}>
|
||||||
<span>{committedText}</span>
|
{chunks.map((chunk) => (
|
||||||
{newText && <span className={classes.fadeIn}>{newText}</span>}
|
<span key={chunk.id} className={isStreaming ? classes.fadeIn : undefined}>
|
||||||
|
{chunk.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user