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(() => {