1
1
"use client" ;
2
2
3
+ import { useLayoutEffect , useRef } from "react" ;
4
+
3
5
interface Message {
4
6
role : string ;
5
7
content : string ;
@@ -17,6 +19,28 @@ interface MessageListProps {
17
19
}
18
20
19
21
export default function MessageList ( { messages } : MessageListProps ) {
22
+ const scrollAreaRef = useRef < HTMLDivElement > ( null ) ;
23
+ // Avoid the message list to change its height all the time. It causes some
24
+ // flickering in the screen because some messages, as the ones displaying
25
+ // progress statuses, are changing the content(the number of lines) and size
26
+ // constantily. To minimize it, we keep track of the biggest scroll height of
27
+ // the content, and use that as the min height of the scroll area.
28
+ const contentMinHeight = useRef ( 0 ) ;
29
+
30
+ useLayoutEffect ( ( ) => {
31
+ if (
32
+ scrollAreaRef . current &&
33
+ scrollAreaRef . current . scrollHeight > contentMinHeight . current
34
+ ) {
35
+ const isFirstScroll = contentMinHeight . current === 0 ;
36
+ scrollAreaRef . current . scrollTo ( {
37
+ top : scrollAreaRef . current . scrollHeight ,
38
+ behavior : isFirstScroll ? "instant" : "smooth" ,
39
+ } ) ;
40
+ contentMinHeight . current = scrollAreaRef . current . scrollHeight ;
41
+ }
42
+ } , [ messages ] ) ;
43
+
20
44
// If no messages, show a placeholder
21
45
if ( messages . length === 0 ) {
22
46
return (
@@ -26,53 +50,12 @@ export default function MessageList({ messages }: MessageListProps) {
26
50
) ;
27
51
}
28
52
29
- // Define a component for the animated dots
30
- const LoadingDots = ( ) => (
31
- < div className = "flex space-x-1" >
32
- < div
33
- aria-hidden = "true"
34
- className = { `size-2 rounded-full bg-foreground animate-pulse [animation-delay:0ms]` }
35
- />
36
- < div
37
- aria-hidden = "true"
38
- className = { `size-2 rounded-full bg-foreground animate-pulse [animation-delay:300ms]` }
39
- />
40
- < div
41
- aria-hidden = "true"
42
- className = { `size-2 rounded-full bg-foreground animate-pulse [animation-delay:600ms]` }
43
- />
44
- < span className = "sr-only" > Loading...</ span >
45
- </ div >
46
- ) ;
47
-
48
53
return (
49
- < div
50
- className = "overflow-y-auto flex-1"
51
- ref = { ( scrollAreaRef ) => {
52
- if ( ! scrollAreaRef ) {
53
- return ;
54
- }
55
-
56
- scrollAreaRef . scrollTo ( {
57
- top : scrollAreaRef . scrollHeight ,
58
- } ) ;
59
-
60
- const callback : MutationCallback = ( mutationsList ) => {
61
- for ( const mutation of mutationsList ) {
62
- if ( mutation . type === "childList" ) {
63
- scrollAreaRef . scrollTo ( {
64
- top : scrollAreaRef . scrollHeight ,
65
- behavior : "smooth" ,
66
- } ) ;
67
- }
68
- }
69
- } ;
70
-
71
- const observer = new MutationObserver ( callback ) ;
72
- observer . observe ( scrollAreaRef , { childList : true , subtree : false } ) ;
73
- } }
74
- >
75
- < div className = "p-4 flex flex-col gap-4 max-w-4xl mx-auto" >
54
+ < div className = "overflow-y-auto flex-1" ref = { scrollAreaRef } >
55
+ < div
56
+ className = "p-4 flex flex-col gap-4 max-w-4xl mx-auto"
57
+ style = { { minHeight : contentMinHeight . current } }
58
+ >
76
59
{ messages . map ( ( message ) => (
77
60
< div
78
61
key = { message . id ?? "draft" }
@@ -93,7 +76,7 @@ export default function MessageList({ messages }: MessageListProps) {
93
76
{ message . role !== "user" && message . content === "" ? (
94
77
< LoadingDots />
95
78
) : (
96
- message . content
79
+ message . content . trim ( )
97
80
) }
98
81
</ div >
99
82
</ div >
@@ -103,3 +86,21 @@ export default function MessageList({ messages }: MessageListProps) {
103
86
</ div >
104
87
) ;
105
88
}
89
+
90
+ const LoadingDots = ( ) => (
91
+ < div className = "flex space-x-1" >
92
+ < div
93
+ aria-hidden = "true"
94
+ className = { `size-2 rounded-full bg-foreground animate-pulse [animation-delay:0ms]` }
95
+ />
96
+ < div
97
+ aria-hidden = "true"
98
+ className = { `size-2 rounded-full bg-foreground animate-pulse [animation-delay:300ms]` }
99
+ />
100
+ < div
101
+ aria-hidden = "true"
102
+ className = { `size-2 rounded-full bg-foreground animate-pulse [animation-delay:600ms]` }
103
+ />
104
+ < span className = "sr-only" > Loading...</ span >
105
+ </ div >
106
+ ) ;
0 commit comments