@xcelsior/ui-chat 2.0.2 → 2.0.4

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.
Files changed (31) hide show
  1. package/.storybook/preview.tsx +2 -1
  2. package/dist/index.d.mts +3 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +49 -53
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +77 -81
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/components/Chat.tsx +35 -74
  10. package/src/components/ChatWidget.tsx +0 -1
  11. package/src/hooks/useMessages.ts +22 -1
  12. package/src/hooks/useWebSocket.ts +18 -1
  13. package/storybook-static/assets/Chat.stories-BkbpOOSG.js +830 -0
  14. package/storybook-static/assets/{Color-YHDXOIA2-BMnd3YrF.js → Color-YHDXOIA2-CSuNIR0a.js} +1 -1
  15. package/storybook-static/assets/{DocsRenderer-CFRXHY34-i_W8iCu9.js → DocsRenderer-CFRXHY34-dpuOKTQp.js} +3 -3
  16. package/storybook-static/assets/{MessageItem-DAaKZ9s9.js → MessageItem-Dlb6dSKL.js} +9 -9
  17. package/storybook-static/assets/MessageItem.stories-CsxqSqu-.js +422 -0
  18. package/storybook-static/assets/{entry-preview-oDnntGcx.js → entry-preview-C_-WO6GJ.js} +1 -1
  19. package/storybook-static/assets/{iframe-CGBtu2Se.js → iframe-BXTccXxS.js} +2 -2
  20. package/storybook-static/assets/preview-B8y-wc-n.css +1 -0
  21. package/storybook-static/assets/preview-CC4t7T7W.js +1 -0
  22. package/storybook-static/assets/{preview-BRpahs9B.js → preview-Cyx3pE7Q.js} +2 -2
  23. package/storybook-static/iframe.html +1 -1
  24. package/storybook-static/index.json +1 -1
  25. package/storybook-static/project.json +1 -1
  26. package/tsconfig.json +4 -0
  27. package/storybook-static/assets/Chat.stories-J_Yp51wU.js +0 -803
  28. package/storybook-static/assets/MessageItem.stories-Ckr1_scc.js +0 -255
  29. package/storybook-static/assets/ToastContext-Bty1K7ya.js +0 -1
  30. package/storybook-static/assets/preview-DUOvJmsz.js +0 -1
  31. package/storybook-static/assets/preview-DcGwT3kv.css +0 -1
@@ -1,8 +1,9 @@
1
1
  import '@xcelsior/design-system/styles';
2
- import type { Preview } from '@storybook/react';
2
+
3
3
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
4
  import { ToastContainer } from '@xcelsior/design-system';
5
5
 
6
+ import type { Preview } from '@storybook/react';
6
7
  const queryClient = new QueryClient({
7
8
  defaultOptions: {
8
9
  queries: {
package/dist/index.d.mts CHANGED
@@ -310,6 +310,9 @@ interface UseWebSocketReturn {
310
310
  /**
311
311
  * Hook for WebSocket connection in chat widget.
312
312
  * Can use an external WebSocket connection (for agents) via the externalWebSocket prop.
313
+ *
314
+ * Handles React Strict Mode (dev) gracefully — tracks whether the effect has been
315
+ * cleaned up so that an aborted connection doesn't trigger reconnection.
313
316
  */
314
317
  declare function useWebSocket(config: IChatConfig, externalWebSocket?: WebSocket | null): UseWebSocketReturn;
315
318
 
package/dist/index.d.ts CHANGED
@@ -310,6 +310,9 @@ interface UseWebSocketReturn {
310
310
  /**
311
311
  * Hook for WebSocket connection in chat widget.
312
312
  * Can use an external WebSocket connection (for agents) via the externalWebSocket prop.
313
+ *
314
+ * Handles React Strict Mode (dev) gracefully — tracks whether the effect has been
315
+ * cleaned up so that an aborted connection doesn't trigger reconnection.
313
316
  */
314
317
  declare function useWebSocket(config: IChatConfig, externalWebSocket?: WebSocket | null): UseWebSocketReturn;
315
318
 
package/dist/index.js CHANGED
@@ -62,6 +62,7 @@ function useWebSocket(config, externalWebSocket) {
62
62
  const reconnectTimeoutRef = (0, import_react.useRef)(null);
63
63
  const reconnectAttemptsRef = (0, import_react.useRef)(0);
64
64
  const messageHandlerRef = (0, import_react.useRef)(null);
65
+ const abortedRef = (0, import_react.useRef)(false);
65
66
  const maxReconnectAttempts = 5;
66
67
  const reconnectDelay = 3e3;
67
68
  const isUsingExternalWs = !!externalWebSocket;
@@ -94,6 +95,7 @@ function useWebSocket(config, externalWebSocket) {
94
95
  };
95
96
  }, []);
96
97
  const connect = (0, import_react.useCallback)(() => {
98
+ if (abortedRef.current) return;
97
99
  console.log("connecting to WebSocket...", config.currentUser, config.conversationId);
98
100
  try {
99
101
  if (wsRef.current) {
@@ -113,6 +115,10 @@ function useWebSocket(config, externalWebSocket) {
113
115
  }
114
116
  const ws = new WebSocket(url.toString());
115
117
  ws.onopen = () => {
118
+ if (abortedRef.current) {
119
+ ws.close(1e3, "Effect cleaned up");
120
+ return;
121
+ }
116
122
  console.log("WebSocket connected");
117
123
  setIsConnected(true);
118
124
  setError(null);
@@ -120,12 +126,14 @@ function useWebSocket(config, externalWebSocket) {
120
126
  config.onConnectionChange?.(true);
121
127
  };
122
128
  ws.onerror = (event) => {
129
+ if (abortedRef.current) return;
123
130
  console.error("WebSocket error:", event);
124
131
  const err = new Error("WebSocket connection error");
125
132
  setError(err);
126
133
  config.onError?.(err);
127
134
  };
128
135
  ws.onclose = (event) => {
136
+ if (abortedRef.current) return;
129
137
  console.log("WebSocket closed:", event.code, event.reason);
130
138
  setIsConnected(false);
131
139
  config.onConnectionChange?.(false);
@@ -174,6 +182,7 @@ function useWebSocket(config, externalWebSocket) {
174
182
  );
175
183
  const reconnect = (0, import_react.useCallback)(() => {
176
184
  reconnectAttemptsRef.current = 0;
185
+ abortedRef.current = false;
177
186
  connect();
178
187
  }, [connect]);
179
188
  (0, import_react.useEffect)(() => {
@@ -183,8 +192,10 @@ function useWebSocket(config, externalWebSocket) {
183
192
  const cleanup = subscribeToMessage(externalWebSocket);
184
193
  return cleanup;
185
194
  }
195
+ abortedRef.current = false;
186
196
  connect();
187
197
  return () => {
198
+ abortedRef.current = true;
188
199
  if (reconnectTimeoutRef.current) {
189
200
  clearTimeout(reconnectTimeoutRef.current);
190
201
  }
@@ -240,6 +251,7 @@ async function fetchMessages(baseUrl, params, headers) {
240
251
  }
241
252
 
242
253
  // src/hooks/useMessages.ts
254
+ var BOT_THINKING_TIMEOUT = 45e3;
243
255
  function useMessages(websocket, config) {
244
256
  const [messages, setMessages] = (0, import_react2.useState)([]);
245
257
  const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
@@ -248,6 +260,7 @@ function useMessages(websocket, config) {
248
260
  const [hasMore, setHasMore] = (0, import_react2.useState)(true);
249
261
  const [isLoadingMore, setIsLoadingMore] = (0, import_react2.useState)(false);
250
262
  const [isBotThinking, setIsBotThinking] = (0, import_react2.useState)(false);
263
+ const botThinkingTimerRef = (0, import_react2.useRef)(null);
251
264
  const { httpApiUrl, conversationId, headers, onError, toast } = config;
252
265
  const headersWithApiKey = (0, import_react2.useMemo)(
253
266
  () => ({
@@ -298,6 +311,10 @@ function useMessages(websocket, config) {
298
311
  });
299
312
  if (newMessage.senderType === "bot" || newMessage.senderType === "system") {
300
313
  setIsBotThinking(false);
314
+ if (botThinkingTimerRef.current) {
315
+ clearTimeout(botThinkingTimerRef.current);
316
+ botThinkingTimerRef.current = null;
317
+ }
301
318
  }
302
319
  onMessageReceived?.(newMessage);
303
320
  }
@@ -311,6 +328,10 @@ function useMessages(websocket, config) {
311
328
  });
312
329
  if (message.senderType === "customer") {
313
330
  setIsBotThinking(true);
331
+ if (botThinkingTimerRef.current) clearTimeout(botThinkingTimerRef.current);
332
+ botThinkingTimerRef.current = setTimeout(() => {
333
+ setIsBotThinking(false);
334
+ }, BOT_THINKING_TIMEOUT);
314
335
  }
315
336
  }, []);
316
337
  const updateMessageStatus = (0, import_react2.useCallback)((messageId, status) => {
@@ -354,6 +375,11 @@ function useMessages(websocket, config) {
354
375
  headersWithApiKey,
355
376
  onError
356
377
  ]);
378
+ (0, import_react2.useEffect)(() => {
379
+ return () => {
380
+ if (botThinkingTimerRef.current) clearTimeout(botThinkingTimerRef.current);
381
+ };
382
+ }, []);
357
383
  return {
358
384
  messages,
359
385
  addMessage,
@@ -2629,7 +2655,6 @@ function ChatWidget({
2629
2655
  })();
2630
2656
  const positionClass = resolvedPosition === "left" ? "left-4" : "right-4";
2631
2657
  const containerStyle = isFullPage ? { backgroundColor: bgColor, color: textColor } : {
2632
- position: "relative",
2633
2658
  width,
2634
2659
  height,
2635
2660
  maxHeight: "calc(100vh - 100px)",
@@ -3346,59 +3371,36 @@ function Chat({
3346
3371
  const [userInfo, setUserInfo] = (0, import_react14.useState)(null);
3347
3372
  const [conversationId, setConversationId] = (0, import_react14.useState)("");
3348
3373
  const [isLoading, setIsLoading] = (0, import_react14.useState)(true);
3349
- const [isAnimating, setIsAnimating] = (0, import_react14.useState)(false);
3350
- const [showWidget, setShowWidget] = (0, import_react14.useState)(false);
3351
3374
  const identityMode = config.identityCollection || "progressive";
3352
3375
  const { position, isDragging, showHint, handlers } = useDraggablePosition(config.position);
3353
- const { currentState, setState: setStateRaw } = useChatWidgetState({
3376
+ const sessionInitializedRef = (0, import_react14.useRef)(false);
3377
+ const { currentState, setState } = useChatWidgetState({
3354
3378
  state,
3355
3379
  defaultState,
3356
3380
  onStateChange
3357
3381
  });
3358
- const setState = (0, import_react14.useCallback)(
3359
- (newState) => {
3360
- if (newState === "open" && currentState === "minimized") {
3361
- setShowWidget(true);
3362
- setIsAnimating(true);
3363
- setStateRaw(newState);
3364
- requestAnimationFrame(() => {
3365
- requestAnimationFrame(() => {
3366
- setIsAnimating(false);
3367
- });
3368
- });
3369
- } else if ((newState === "minimized" || newState === "closed") && currentState === "open") {
3370
- setIsAnimating(true);
3371
- setTimeout(() => {
3372
- setShowWidget(false);
3373
- setIsAnimating(false);
3374
- setStateRaw(newState);
3375
- }, 200);
3376
- } else {
3377
- setStateRaw(newState);
3378
- }
3379
- },
3380
- [currentState, setStateRaw]
3381
- );
3382
- (0, import_react14.useEffect)(() => {
3383
- if (currentState === "open") {
3384
- setShowWidget(true);
3385
- }
3386
- }, [currentState]);
3382
+ const configConversationId = config.conversationId;
3383
+ const configUserEmail = config.currentUser?.email;
3384
+ const configUserName = config.currentUser?.name;
3385
+ const configUserAvatar = config.currentUser?.avatar;
3386
+ const configUserStatus = config.currentUser?.status;
3387
3387
  (0, import_react14.useEffect)(() => {
3388
+ if (sessionInitializedRef.current) return;
3388
3389
  const initializeSession = () => {
3389
3390
  try {
3390
- if (config.currentUser?.email && config.currentUser?.name) {
3391
- const convId2 = config.conversationId || generateSessionId();
3391
+ if (configUserEmail && configUserName) {
3392
+ const convId2 = configConversationId || generateSessionId();
3392
3393
  const user = {
3393
- name: config.currentUser.name,
3394
- email: config.currentUser.email,
3395
- avatar: config.currentUser.avatar,
3394
+ name: configUserName,
3395
+ email: configUserEmail,
3396
+ avatar: configUserAvatar,
3396
3397
  type: "customer",
3397
- status: config.currentUser.status
3398
+ status: configUserStatus
3398
3399
  };
3399
3400
  setUserInfo(user);
3400
3401
  setConversationId(convId2);
3401
3402
  setIsLoading(false);
3403
+ sessionInitializedRef.current = true;
3402
3404
  return;
3403
3405
  }
3404
3406
  const storedDataJson = localStorage.getItem(`${storageKeyPrefix}_user`);
@@ -3415,23 +3417,26 @@ function Chat({
3415
3417
  setUserInfo(user);
3416
3418
  setConversationId(storedData.conversationId);
3417
3419
  setIsLoading(false);
3420
+ sessionInitializedRef.current = true;
3418
3421
  return;
3419
3422
  }
3420
3423
  }
3421
- const convId = config.conversationId || generateSessionId();
3424
+ const convId = configConversationId || generateSessionId();
3422
3425
  setConversationId(convId);
3423
3426
  if (identityMode === "progressive" || identityMode === "none") {
3424
3427
  setUserInfo(null);
3425
3428
  }
3429
+ sessionInitializedRef.current = true;
3426
3430
  } catch (error) {
3427
3431
  console.error("Error initializing chat session:", error);
3428
- setConversationId(config.conversationId || generateSessionId());
3432
+ setConversationId(configConversationId || generateSessionId());
3433
+ sessionInitializedRef.current = true;
3429
3434
  } finally {
3430
3435
  setIsLoading(false);
3431
3436
  }
3432
3437
  };
3433
3438
  initializeSession();
3434
- }, [config, storageKeyPrefix, identityMode]);
3439
+ }, [configConversationId, configUserEmail, configUserName, configUserAvatar, configUserStatus, storageKeyPrefix, identityMode]);
3435
3440
  const handlePreChatSubmit = (0, import_react14.useCallback)(
3436
3441
  (name, email) => {
3437
3442
  const convId = conversationId || generateSessionId();
@@ -3517,16 +3522,7 @@ function Chat({
3517
3522
  conversationId,
3518
3523
  currentUser: userInfo || void 0
3519
3524
  };
3520
- const widgetAnimationStyle = showWidget && !isAnimating ? {
3521
- opacity: 1,
3522
- transform: "translateY(0) scale(1)",
3523
- transition: "opacity 0.25s ease-out, transform 0.25s ease-out"
3524
- } : {
3525
- opacity: 0,
3526
- transform: "translateY(12px) scale(0.97)",
3527
- transition: "opacity 0.2s ease-in, transform 0.2s ease-in"
3528
- };
3529
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: widgetAnimationStyle, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3525
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3530
3526
  ChatWidget,
3531
3527
  {
3532
3528
  config: fullConfig,
@@ -3535,7 +3531,7 @@ function Chat({
3535
3531
  onMinimize: () => setState("minimized"),
3536
3532
  resolvedPosition: position
3537
3533
  }
3538
- ) });
3534
+ );
3539
3535
  }
3540
3536
 
3541
3537
  // src/components/TypingIndicator.tsx