@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/ui-chat",
3
- "version": "2.0.1",
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",
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useRef } from 'react';
2
2
 
3
- import { Spinner } from '@xcelsior/design-system';
3
+ import { Spinner } from './Spinner';
4
4
 
5
5
  import { XcelsiorAvatar, XcelsiorSymbol } from './BrandIcons';
6
6
  import { MessageItem } from './MessageItem';
@@ -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
  }