Stijnus commited on
Commit
9e8d05c
·
2 Parent(s): 547cde7 d966656

Merge branch 'FEAT_BoltDYI_CHAT_FIX' into FEAT_BoltDYI_NEW_SETTINGS_UI_V2

Browse files
app/components/chat/Messages.client.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import type { Message } from 'ai';
2
- import React, { Fragment } from 'react';
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
@@ -19,6 +19,94 @@ interface MessagesProps {
19
  export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
20
  const { id, isStreaming = false, messages = [] } = props;
21
  const location = useLocation();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  const handleRewind = (messageId: string) => {
24
  const searchParams = new URLSearchParams(location.search);
@@ -41,7 +129,20 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
41
  };
42
 
43
  return (
44
- <div id={id} ref={ref} className={props.className}>
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  {messages.length > 0
46
  ? messages.map((message, index) => {
47
  const { role, content, id: messageId, annotations } = message;
@@ -107,6 +208,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
107
  );
108
  })
109
  : null}
 
110
  {isStreaming && (
111
  <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
112
  )}
 
1
  import type { Message } from 'ai';
2
+ import React, { Fragment, useEffect, useRef, useState } from 'react';
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
 
19
  export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
20
  const { id, isStreaming = false, messages = [] } = props;
21
  const location = useLocation();
22
+ const messagesEndRef = useRef<HTMLDivElement>(null);
23
+ const containerRef = useRef<HTMLDivElement>(null);
24
+ const [isUserInteracting, setIsUserInteracting] = useState(false);
25
+ const [lastScrollTop, setLastScrollTop] = useState(0);
26
+ const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
27
+
28
+ // Check if we should auto-scroll based on scroll position
29
+ const checkShouldAutoScroll = () => {
30
+ if (!containerRef.current) {
31
+ return true;
32
+ }
33
+
34
+ const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
35
+ const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
36
+
37
+ return distanceFromBottom < 100;
38
+ };
39
+
40
+ const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
41
+ if (!shouldAutoScroll || isUserInteracting) {
42
+ return;
43
+ }
44
+
45
+ messagesEndRef.current?.scrollIntoView({ behavior });
46
+ };
47
+
48
+ // Handle user interaction and scroll position
49
+ useEffect(() => {
50
+ const container = containerRef.current;
51
+
52
+ if (!container) {
53
+ return undefined;
54
+ }
55
+
56
+ const handleInteractionStart = () => {
57
+ setIsUserInteracting(true);
58
+ };
59
+
60
+ const handleInteractionEnd = () => {
61
+ if (checkShouldAutoScroll()) {
62
+ setTimeout(() => setIsUserInteracting(false), 100);
63
+ }
64
+ };
65
+
66
+ const handleScroll = () => {
67
+ const { scrollTop } = container;
68
+ const shouldScroll = checkShouldAutoScroll();
69
+
70
+ // Update auto-scroll state based on scroll position
71
+ setShouldAutoScroll(shouldScroll);
72
+
73
+ // If scrolling up, disable auto-scroll
74
+ if (scrollTop < lastScrollTop) {
75
+ setIsUserInteracting(true);
76
+ }
77
+
78
+ setLastScrollTop(scrollTop);
79
+ };
80
+
81
+ container.addEventListener('mousedown', handleInteractionStart);
82
+ container.addEventListener('mouseup', handleInteractionEnd);
83
+ container.addEventListener('touchstart', handleInteractionStart);
84
+ container.addEventListener('touchend', handleInteractionEnd);
85
+ container.addEventListener('scroll', handleScroll, { passive: true });
86
+
87
+ return () => {
88
+ container.removeEventListener('mousedown', handleInteractionStart);
89
+ container.removeEventListener('mouseup', handleInteractionEnd);
90
+ container.removeEventListener('touchstart', handleInteractionStart);
91
+ container.removeEventListener('touchend', handleInteractionEnd);
92
+ container.removeEventListener('scroll', handleScroll);
93
+ };
94
+ }, [lastScrollTop]);
95
+
96
+ // Scroll to bottom when new messages are added or during streaming
97
+ useEffect(() => {
98
+ if (messages.length > 0 && (isStreaming || shouldAutoScroll)) {
99
+ scrollToBottom('smooth');
100
+ }
101
+ }, [messages, isStreaming, shouldAutoScroll]);
102
+
103
+ // Initial scroll on component mount
104
+ useEffect(() => {
105
+ if (messages.length > 0) {
106
+ scrollToBottom('instant');
107
+ setShouldAutoScroll(true);
108
+ }
109
+ }, []);
110
 
111
  const handleRewind = (messageId: string) => {
112
  const searchParams = new URLSearchParams(location.search);
 
129
  };
130
 
131
  return (
132
+ <div
133
+ id={id}
134
+ ref={(el) => {
135
+ // Combine refs
136
+ if (typeof ref === 'function') {
137
+ ref(el);
138
+ }
139
+
140
+ (containerRef as any).current = el;
141
+
142
+ return undefined;
143
+ }}
144
+ className={props.className}
145
+ >
146
  {messages.length > 0
147
  ? messages.map((message, index) => {
148
  const { role, content, id: messageId, annotations } = message;
 
208
  );
209
  })
210
  : null}
211
+ <div ref={messagesEndRef} /> {/* Add an empty div as scroll anchor */}
212
  {isStreaming && (
213
  <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
214
  )}