1
1
import { WS_URL } from "astro:env/client" ;
2
2
import useWebSocket , { ReadyState } from "react-use-websocket" ;
3
- import React , { useState , useCallback } from "react" ;
3
+ import React , { useState , useCallback , useEffect } from "react" ;
4
4
import ChatInput from "./ChatInput.tsx" ;
5
5
import ChatMessages from "./ChatMessages.tsx" ;
6
6
import type { ChatMessage } from "./ChatMessage.tsx" ;
@@ -21,7 +21,9 @@ export default function Chat({ username }: { username: string }) {
21
21
22
22
const [ input , setInput ] = useState ( "" ) ;
23
23
const [ messageHistory , setMessageHistory ] = useState < ChatMessage [ ] > ( [ ] ) ;
24
- const [ isLoading , setIsLoading ] = useState ( false ) ; // Add this line
24
+ const [ isLoading , setIsLoading ] = useState ( false ) ;
25
+ const [ connectionStatus , setConnectionStatus ] = useState < string > ( "Connecting..." ) ;
26
+ const [ isReconnecting , setIsReconnecting ] = useState ( false ) ;
25
27
26
28
const handleMessage = useCallback ( ( event : WebSocketEventMap [ "message" ] ) => {
27
29
console . log ( event ) ;
@@ -37,25 +39,92 @@ export default function Chat({ username }: { username: string }) {
37
39
} catch ( e ) {
38
40
console . debug ( "Invalid JSON message received:" , event . data ) ;
39
41
}
40
- setIsLoading ( false ) ; // Add this line
42
+ setIsLoading ( false ) ;
41
43
} , [ ] ) ;
42
44
43
45
const { sendJsonMessage, readyState } = useWebSocket < ChatMessage > ( WS_URL , {
44
- onOpen : ( ) => console . log ( "WebSocket connection opened!" ) ,
45
- onClose : ( event ) => console . log ( "WebSocket connection closed!: " , event ) ,
46
- onError : ( event ) => console . error ( "WebSocket error:" , event ) ,
46
+ onOpen : ( ) => {
47
+ console . log ( "WebSocket connection opened!" ) ;
48
+ setConnectionStatus ( "Connected" ) ;
49
+ setIsReconnecting ( false ) ;
50
+ } ,
51
+ onClose : ( event ) => {
52
+ console . log ( "WebSocket connection closed!: " , event ) ;
53
+ // Only show disconnected if we're not actively reconnecting
54
+ if ( ! isReconnecting ) {
55
+ setConnectionStatus ( "Disconnected" ) ;
56
+ }
57
+ } ,
58
+ onError : ( event ) => {
59
+ console . error ( "WebSocket error:" , event ) ;
60
+ setConnectionStatus ( "Error - Retrying..." ) ;
61
+ setIsReconnecting ( true ) ;
62
+ } ,
47
63
onMessage : handleMessage ,
64
+ onReconnectStop : ( ) => {
65
+ console . log ( "Reconnection attempts stopped" ) ;
66
+ setConnectionStatus ( "Disconnected" ) ;
67
+ setIsReconnecting ( false ) ;
68
+ } ,
69
+ // Add retry configuration
70
+ shouldReconnect : ( closeEvent ) => {
71
+ console . log ( "Should reconnect:" , closeEvent ) ;
72
+ setIsReconnecting ( true ) ;
73
+ setConnectionStatus ( "Reconnecting..." ) ;
74
+ return true ; // Always try to reconnect
75
+ } ,
76
+ reconnectInterval : ( attemptNumber ) => {
77
+ // Exponential backoff: 1s, 2s, 4s, 8s, max 30s
78
+ return Math . min ( 1000 * Math . pow ( 2 , attemptNumber ) , 30000 ) ;
79
+ } ,
80
+ reconnectAttempts : 10 ,
81
+ // Connection timeout
82
+ heartbeat : {
83
+ message : JSON . stringify ( { type : "ping" } ) ,
84
+ returnMessage : JSON . stringify ( { type : "pong" } ) ,
85
+ timeout : 60000 , // 60 seconds
86
+ interval : 60000 , // 60 seconds
87
+ } ,
48
88
} ) ;
49
89
90
+ // Update connection status based on readyState
91
+ useEffect ( ( ) => {
92
+ switch ( readyState ) {
93
+ case ReadyState . CONNECTING :
94
+ if ( ! isReconnecting ) {
95
+ setConnectionStatus ( "Connecting..." ) ;
96
+ }
97
+ break ;
98
+ case ReadyState . OPEN :
99
+ setConnectionStatus ( "Connected" ) ;
100
+ setIsReconnecting ( false ) ;
101
+ break ;
102
+ case ReadyState . CLOSING :
103
+ setConnectionStatus ( "Disconnecting..." ) ;
104
+ break ;
105
+ case ReadyState . CLOSED :
106
+ if ( ! isReconnecting ) {
107
+ setConnectionStatus ( "Disconnected" ) ;
108
+ }
109
+ break ;
110
+ case ReadyState . UNINSTANTIATED :
111
+ setConnectionStatus ( "Not Connected" ) ;
112
+ break ;
113
+ }
114
+ } , [ readyState , isReconnecting ] ) ;
115
+
50
116
const handleInputChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
51
117
setInput ( event . target . value ) ;
52
118
} ;
53
119
54
120
const handleSubmit = ( event : React . FormEvent < HTMLFormElement > ) => {
55
121
event . preventDefault ( ) ;
56
122
if ( input . trim ( ) === "" ) return ;
123
+
124
+ // Only prevent submission if truly disconnected (not reconnecting)
125
+ if ( readyState !== ReadyState . OPEN && ! isReconnecting ) return ;
57
126
58
- setIsLoading ( true ) ; // Add this line
127
+ setIsLoading ( true ) ;
59
128
60
129
const chatMessage : ChatMessage = {
61
130
role : "client" ,
@@ -79,8 +148,29 @@ export default function Chat({ username }: { username: string }) {
79
148
80
149
console . log ( clientMessage ) ;
81
150
82
- sendJsonMessage ( clientMessage ) ;
83
- setInput ( "" ) ;
151
+ try {
152
+ sendJsonMessage ( clientMessage ) ;
153
+ setInput ( "" ) ;
154
+ } catch ( error ) {
155
+ console . error ( "Failed to send message:" , error ) ;
156
+ setIsLoading ( false ) ;
157
+ // Optionally show error message to user
158
+ }
159
+ } ;
160
+
161
+ const getConnectionStatusColor = ( ) => {
162
+ if ( isReconnecting ) {
163
+ return "text-yellow-500" ; // Show yellow during reconnection attempts
164
+ }
165
+
166
+ switch ( readyState ) {
167
+ case ReadyState . OPEN :
168
+ return "text-green-500" ;
169
+ case ReadyState . CONNECTING :
170
+ return "text-yellow-500" ;
171
+ default :
172
+ return "text-red-500" ;
173
+ }
84
174
} ;
85
175
86
176
return (
@@ -95,17 +185,22 @@ export default function Chat({ username }: { username: string }) {
95
185
< div className = "fixed bottom-32 right-4 bg-gray-800 text-white rounded-lg shadow-lg flex flex-col w-11/12 md:w-1/3" >
96
186
< div className = "flex justify-between items-center p-4 bg-blue-500 rounded-t-lg" >
97
187
< h1 className = "text-xl font-bold" > Big Friendly Bank</ h1 >
98
- < button onClick = { toggleChat } className = "text-white" >
99
- X
100
- </ button >
188
+ < div className = "flex items-center gap-2" >
189
+ < span className = { `text-sm ${ getConnectionStatusColor ( ) } ` } >
190
+ { connectionStatus }
191
+ </ span >
192
+ < button onClick = { toggleChat } className = "text-white" >
193
+ X
194
+ </ button >
195
+ </ div >
101
196
</ div >
102
197
< div className = "flex flex-col flex-grow w-full max-w-screen-lg rounded-lg h-full overflow-y-auto" >
103
198
< ChatMessages messages = { messageHistory } isLoading = { isLoading } />
104
199
< ChatInput
105
200
input = { input }
106
201
handleSubmit = { handleSubmit }
107
202
handleInputChange = { handleInputChange }
108
- isDisabled = { readyState !== ReadyState . OPEN }
203
+ isDisabled = { readyState !== ReadyState . OPEN && ! isReconnecting }
109
204
/>
110
205
</ div >
111
206
</ div >
0 commit comments