diff --git a/.changeset/tasty-goats-wink.md b/.changeset/tasty-goats-wink.md new file mode 100644 index 000000000..d82b8703c --- /dev/null +++ b/.changeset/tasty-goats-wink.md @@ -0,0 +1,6 @@ +--- +"@assistant-ui/react-ui": patch +"@assistant-ui/react": patch +--- + +feat: smooth streaming by default diff --git a/packages/react-ui/src/components/markdown-text.tsx b/packages/react-ui/src/components/markdown-text.tsx index fccec728e..59c2be3bf 100644 --- a/packages/react-ui/src/components/markdown-text.tsx +++ b/packages/react-ui/src/components/markdown-text.tsx @@ -7,14 +7,16 @@ type MarkdownTextProps = Partial; export const makeMarkdownText = ({ className, + smooth = true, ...rest }: MarkdownTextProps = {}) => { const MarkdownTextImpl: FC = ({ status }) => { return ( = ({ status }) => { "aui-text" + (status === "in_progress" ? " aui-text-in-progress" : "") } > - +

); }; diff --git a/packages/react/src/primitives/contentPart/ContentPartText.tsx b/packages/react/src/primitives/contentPart/ContentPartText.tsx index 572599602..3fc21a083 100644 --- a/packages/react/src/primitives/contentPart/ContentPartText.tsx +++ b/packages/react/src/primitives/contentPart/ContentPartText.tsx @@ -14,7 +14,7 @@ export type ContentPartPrimitiveTextProps = Omit< export const ContentPartPrimitiveText = forwardRef< ContentPartPrimitiveTextElement, ContentPartPrimitiveTextProps ->(({ smooth, ...rest }, forwardedRef) => { +>(({ smooth = true, ...rest }, forwardedRef) => { const { status, part: { text }, diff --git a/packages/react/src/primitives/message/MessageContent.tsx b/packages/react/src/primitives/message/MessageContent.tsx index 37ea17810..86687af8c 100644 --- a/packages/react/src/primitives/message/MessageContent.tsx +++ b/packages/react/src/primitives/message/MessageContent.tsx @@ -39,7 +39,7 @@ export type MessagePrimitiveContentProps = { const defaultComponents = { Text: () => (

- + {" \u25CF"} diff --git a/packages/react/src/utils/hooks/useSmooth.tsx b/packages/react/src/utils/hooks/useSmooth.tsx index 467895614..7e67a9415 100644 --- a/packages/react/src/utils/hooks/useSmooth.tsx +++ b/packages/react/src/utils/hooks/useSmooth.tsx @@ -3,7 +3,6 @@ import { useEffect, useState } from "react"; class TextStreamAnimator { private animationFrameId: number | null = null; private lastUpdateTime: number = Date.now(); - private decayFactor: number = 0.99; public targetText: string = ""; @@ -13,6 +12,7 @@ class TextStreamAnimator { start() { if (this.animationFrameId !== null) return; + this.lastUpdateTime = Date.now(); this.animate(); } @@ -26,7 +26,7 @@ class TextStreamAnimator { private animate = () => { const currentTime = Date.now(); const deltaTime = currentTime - this.lastUpdateTime; - this.lastUpdateTime = currentTime; + let timeToConsume = deltaTime; this.setText((currentText) => { const targetText = this.targetText; @@ -37,14 +37,22 @@ class TextStreamAnimator { } const remainingChars = targetText.length - currentText.length; - const charsToAdd = Math.max( - 1, - Math.floor( - remainingChars * (1 - Math.pow(this.decayFactor, deltaTime)), - ), - ); - const newText = targetText.slice(0, currentText.length + charsToAdd); + const baseTimePerChar = Math.min(5, 250 / remainingChars); + + let charsToAdd = 0; + while (timeToConsume >= baseTimePerChar && charsToAdd < remainingChars) { + charsToAdd++; + timeToConsume -= baseTimePerChar; + } + this.animationFrameId = requestAnimationFrame(this.animate); + + if (charsToAdd === 0) { + return currentText; + } + + const newText = targetText.slice(0, currentText.length + charsToAdd); + this.lastUpdateTime = currentTime - timeToConsume; return newText; }); }; @@ -57,6 +65,7 @@ export const useSmooth = (text: string, smooth: boolean = false) => { ); useEffect(() => { + console.log("smooth", smooth); if (!smooth) { animatorRef.stop(); return; @@ -71,6 +80,7 @@ export const useSmooth = (text: string, smooth: boolean = false) => { animatorRef.targetText = text; animatorRef.start(); + console.log("animating"); }, [animatorRef, smooth, text]); useEffect(() => {