@@ -89,6 +89,16 @@ export interface BubbleProps
8989 * @default "solid"
9090 */
9191 background ?: "transparent" | "solid" ;
92+ /**
93+ * Custom pending content to display when pending is true.
94+ * @description If not provided, will use default dots animation.
95+ */
96+ pending ?: React . ReactNode ;
97+ /**
98+ * Whether the bubble is in pending state.
99+ * @default false
100+ */
101+ isPending ?: boolean ;
92102}
93103
94104export function Bubble ( {
@@ -97,10 +107,26 @@ export function Bubble({
97107 size,
98108 align,
99109 background = "solid" ,
110+ pending,
111+ isPending = false ,
100112 ...props
101113} : BubbleProps ) {
102114 const { isDark } = useTheme ( ) ;
103115
116+ const defaultPending = (
117+ < div className = "flex items-center space-x-1 py-1" >
118+ < div className = "w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
119+ < div
120+ className = "w-2 h-2 bg-gray-400 rounded-full animate-bounce"
121+ style = { { animationDelay : "0.1s" } }
122+ />
123+ < div
124+ className = "w-2 h-2 bg-gray-400 rounded-full animate-bounce"
125+ style = { { animationDelay : "0.2s" } }
126+ />
127+ </ div >
128+ ) ;
129+
104130 return (
105131 < div
106132 data-slot = "bubble"
@@ -112,50 +138,55 @@ export function Bubble({
112138 align,
113139 background,
114140 } ) ,
141+ pending && "flex items-center" ,
115142 ) ,
116143 ) }
117144 { ...props }
118145 >
119- < Markdown
120- remarkPlugins = { [ remarkGfm , remarkMath ] }
121- components = { {
122- code ( props ) {
123- const { children, className, ref : _ref , ...rest } = props ;
124- const match = / l a n g u a g e - ( \w + ) / . exec ( className || "" ) ;
125- return match ? (
126- < div className = "w-full overflow-x-auto border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-800 rounded-lg" >
127- < SyntaxHighlighter
128- { ...rest }
129- PreTag = "div"
130- language = { match [ 1 ] }
131- style = { isDark ? vscDarkPlus : oneLight }
132- customStyle = { {
133- background : "transparent" ,
134- margin : 0 ,
135- padding : "1rem" ,
136- borderRadius : "0.5rem" ,
137- overflowX : "auto" ,
138- } }
139- codeTagProps = { {
140- style : {
141- fontFamily : "monospace" ,
142- fontSize : "0.875rem" ,
143- } ,
144- } }
145- >
146- { String ( children ) . replace ( / \n $ / , "" ) }
147- </ SyntaxHighlighter >
148- </ div >
149- ) : (
150- < code { ...rest } className = { className } >
151- { children }
152- </ code >
153- ) ;
154- } ,
155- } }
156- >
157- { text }
158- </ Markdown >
146+ { isPending ? (
147+ pending || defaultPending
148+ ) : (
149+ < Markdown
150+ remarkPlugins = { [ remarkGfm , remarkMath ] }
151+ components = { {
152+ code ( props ) {
153+ const { children, className, ref : _ref , ...rest } = props ;
154+ const match = / l a n g u a g e - ( \w + ) / . exec ( className || "" ) ;
155+ return match ? (
156+ < div className = "w-full overflow-x-auto border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-800 rounded-lg" >
157+ < SyntaxHighlighter
158+ { ...rest }
159+ PreTag = "div"
160+ language = { match [ 1 ] }
161+ style = { isDark ? vscDarkPlus : oneLight }
162+ customStyle = { {
163+ background : "transparent" ,
164+ margin : 0 ,
165+ padding : "1rem" ,
166+ borderRadius : "0.5rem" ,
167+ overflowX : "auto" ,
168+ } }
169+ codeTagProps = { {
170+ style : {
171+ fontFamily : "monospace" ,
172+ fontSize : "0.875rem" ,
173+ } ,
174+ } }
175+ >
176+ { String ( children ) . replace ( / \n $ / , "" ) }
177+ </ SyntaxHighlighter >
178+ </ div >
179+ ) : (
180+ < code { ...rest } className = { className } >
181+ { children }
182+ </ code >
183+ ) ;
184+ } ,
185+ } }
186+ >
187+ { text }
188+ </ Markdown >
189+ ) }
159190 </ div >
160191 ) ;
161192}
@@ -202,13 +233,27 @@ export interface BubbleListProps extends React.ComponentProps<"div"> {
202233 * @default "right-solid"
203234 */
204235 background ?: "transparent" | "solid" | "left-solid" | "right-solid" ;
236+ isPending ?: boolean ;
237+ assistant ?: {
238+ avatar ?: AvatarProps ;
239+ align ?: "left" | "right" ;
240+ } ;
205241 footer ?: React . ReactNode ;
242+ pending ?: React . ReactNode ;
206243}
207244
208245export function BubbleList ( {
209246 className,
210247 background = "right-solid" ,
211248 footer,
249+ pending,
250+ assistant = {
251+ avatar : {
252+ text : "A" ,
253+ } ,
254+ align : "left" ,
255+ } ,
256+ isPending = true ,
212257 ...props
213258} : BubbleListProps ) {
214259 const { messages } = props ;
@@ -222,7 +267,7 @@ export function BubbleList({
222267 block : "end" ,
223268 } ) ;
224269 }
225- } , [ messages ] ) ;
270+ } , [ messages , isPending ] ) ;
226271
227272 return (
228273 < div
@@ -269,6 +314,33 @@ export function BubbleList({
269314 />
270315 </ div >
271316 ) ) }
317+ { isPending && (
318+ < div
319+ key = "pending"
320+ data-slot = "bubble-item"
321+ className = { twMerge (
322+ clsx ( assistant ?. align === "right" && "flex-row-reverse" ) ,
323+ "flex items-start gap-2 w-full" ,
324+ ) }
325+ >
326+ < Avatar className = "flex-shrink-0" { ...( assistant ?. avatar || { } ) } />
327+ < Bubble
328+ isPending = { isPending }
329+ pending = { pending }
330+ text = ""
331+ align = { assistant ?. align || "left" }
332+ background = {
333+ ( background === "left-solid" &&
334+ ( assistant ?. align || "left" ) === "left" ) ||
335+ ( background === "right-solid" &&
336+ ( assistant ?. align || "left" ) === "right" ) ||
337+ background === "solid"
338+ ? "solid"
339+ : "transparent"
340+ }
341+ />
342+ </ div >
343+ ) }
272344 </ div >
273345 { footer && (
274346 < div
0 commit comments