@xcelsior/ui-chat 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +236 -182
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +236 -182
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -6
- package/src/components/MessageList.tsx +1 -1
- package/src/components/Spinner.tsx +50 -0
- package/src/hooks/useWebSocket.ts +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcelsior/ui-chat",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,14 +21,10 @@
|
|
|
21
21
|
"storybook": "^8.6.14",
|
|
22
22
|
"@tailwindcss/postcss": "^4.1.13",
|
|
23
23
|
"tsup": "^8.0.2",
|
|
24
|
-
"typescript": "^5.3.3"
|
|
25
|
-
"@xcelsior/design-system": "1.0.11",
|
|
26
|
-
"@xcelsior/ui-fields": "1.0.6"
|
|
24
|
+
"typescript": "^5.3.3"
|
|
27
25
|
},
|
|
28
26
|
"peerDependencies": {
|
|
29
27
|
"tailwindcss": "^4",
|
|
30
|
-
"@xcelsior/design-system": "^1.0.8",
|
|
31
|
-
"@xcelsior/ui-fields": "^1.0.4",
|
|
32
28
|
"flowbite-react": "^0.12",
|
|
33
29
|
"react": "^18.2.0",
|
|
34
30
|
"react-dom": "^18.2.0",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface SpinnerProps {
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SIZE_MAP: Record<NonNullable<SpinnerProps['size']>, number> = {
|
|
9
|
+
sm: 16,
|
|
10
|
+
md: 24,
|
|
11
|
+
lg: 36,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const BORDER_MAP: Record<NonNullable<SpinnerProps['size']>, number> = {
|
|
15
|
+
sm: 2,
|
|
16
|
+
md: 2,
|
|
17
|
+
lg: 3,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function Spinner({ size = 'md', color = 'currentColor' }: SpinnerProps) {
|
|
21
|
+
const px = SIZE_MAP[size];
|
|
22
|
+
const border = BORDER_MAP[size];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<style>{`
|
|
27
|
+
@keyframes xchat-spin {
|
|
28
|
+
to { transform: rotate(360deg); }
|
|
29
|
+
}
|
|
30
|
+
.xchat-spinner {
|
|
31
|
+
animation: xchat-spin 0.75s linear infinite;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
display: inline-block;
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
}
|
|
36
|
+
`}</style>
|
|
37
|
+
<span
|
|
38
|
+
className="xchat-spinner"
|
|
39
|
+
role="status"
|
|
40
|
+
aria-label="Loading"
|
|
41
|
+
style={{
|
|
42
|
+
width: px,
|
|
43
|
+
height: px,
|
|
44
|
+
border: `${border}px solid rgba(128,128,128,0.25)`,
|
|
45
|
+
borderTopColor: color,
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -12,6 +12,9 @@ export interface UseWebSocketReturn {
|
|
|
12
12
|
/**
|
|
13
13
|
* Hook for WebSocket connection in chat widget.
|
|
14
14
|
* Can use an external WebSocket connection (for agents) via the externalWebSocket prop.
|
|
15
|
+
*
|
|
16
|
+
* Handles React Strict Mode (dev) gracefully — tracks whether the effect has been
|
|
17
|
+
* cleaned up so that an aborted connection doesn't trigger reconnection.
|
|
15
18
|
*/
|
|
16
19
|
export function useWebSocket(
|
|
17
20
|
config: IChatConfig,
|
|
@@ -24,6 +27,7 @@ export function useWebSocket(
|
|
|
24
27
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
25
28
|
const reconnectAttemptsRef = useRef(0);
|
|
26
29
|
const messageHandlerRef = useRef<((event: MessageEvent) => void) | null>(null);
|
|
30
|
+
const abortedRef = useRef(false);
|
|
27
31
|
const maxReconnectAttempts = 5;
|
|
28
32
|
const reconnectDelay = 3000;
|
|
29
33
|
|
|
@@ -71,6 +75,9 @@ export function useWebSocket(
|
|
|
71
75
|
|
|
72
76
|
// biome-ignore lint/correctness/useExhaustiveDependencies: dependencies managed manually
|
|
73
77
|
const connect = useCallback(() => {
|
|
78
|
+
// Skip if the effect was already cleaned up (React Strict Mode)
|
|
79
|
+
if (abortedRef.current) return;
|
|
80
|
+
|
|
74
81
|
console.log('connecting to WebSocket...', config.currentUser, config.conversationId);
|
|
75
82
|
try {
|
|
76
83
|
// Clean up existing connection
|
|
@@ -96,6 +103,10 @@ export function useWebSocket(
|
|
|
96
103
|
const ws = new WebSocket(url.toString());
|
|
97
104
|
|
|
98
105
|
ws.onopen = () => {
|
|
106
|
+
if (abortedRef.current) {
|
|
107
|
+
ws.close(1000, 'Effect cleaned up');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
99
110
|
console.log('WebSocket connected');
|
|
100
111
|
setIsConnected(true);
|
|
101
112
|
setError(null);
|
|
@@ -104,6 +115,7 @@ export function useWebSocket(
|
|
|
104
115
|
};
|
|
105
116
|
|
|
106
117
|
ws.onerror = (event) => {
|
|
118
|
+
if (abortedRef.current) return;
|
|
107
119
|
console.error('WebSocket error:', event);
|
|
108
120
|
const err = new Error('WebSocket connection error');
|
|
109
121
|
setError(err);
|
|
@@ -111,6 +123,7 @@ export function useWebSocket(
|
|
|
111
123
|
};
|
|
112
124
|
|
|
113
125
|
ws.onclose = (event) => {
|
|
126
|
+
if (abortedRef.current) return;
|
|
114
127
|
console.log('WebSocket closed:', event.code, event.reason);
|
|
115
128
|
setIsConnected(false);
|
|
116
129
|
config.onConnectionChange?.(false);
|
|
@@ -165,6 +178,7 @@ export function useWebSocket(
|
|
|
165
178
|
|
|
166
179
|
const reconnect = useCallback(() => {
|
|
167
180
|
reconnectAttemptsRef.current = 0;
|
|
181
|
+
abortedRef.current = false;
|
|
168
182
|
connect();
|
|
169
183
|
}, [connect]);
|
|
170
184
|
|
|
@@ -176,10 +190,13 @@ export function useWebSocket(
|
|
|
176
190
|
return cleanup;
|
|
177
191
|
}
|
|
178
192
|
|
|
193
|
+
// Reset abort flag for this effect cycle
|
|
194
|
+
abortedRef.current = false;
|
|
179
195
|
connect();
|
|
180
196
|
|
|
181
|
-
// Cleanup on unmount
|
|
197
|
+
// Cleanup on unmount (or React Strict Mode re-run)
|
|
182
198
|
return () => {
|
|
199
|
+
abortedRef.current = true;
|
|
183
200
|
if (reconnectTimeoutRef.current) {
|
|
184
201
|
clearTimeout(reconnectTimeoutRef.current);
|
|
185
202
|
}
|