@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
package/dist/index.mjs CHANGED
@@ -11,6 +11,7 @@ function useWebSocket(config, externalWebSocket) {
11
11
  const reconnectTimeoutRef = useRef(null);
12
12
  const reconnectAttemptsRef = useRef(0);
13
13
  const messageHandlerRef = useRef(null);
14
+ const abortedRef = useRef(false);
14
15
  const maxReconnectAttempts = 5;
15
16
  const reconnectDelay = 3e3;
16
17
  const isUsingExternalWs = !!externalWebSocket;
@@ -43,6 +44,7 @@ function useWebSocket(config, externalWebSocket) {
43
44
  };
44
45
  }, []);
45
46
  const connect = useCallback(() => {
47
+ if (abortedRef.current) return;
46
48
  console.log("connecting to WebSocket...", config.currentUser, config.conversationId);
47
49
  try {
48
50
  if (wsRef.current) {
@@ -62,6 +64,10 @@ function useWebSocket(config, externalWebSocket) {
62
64
  }
63
65
  const ws = new WebSocket(url.toString());
64
66
  ws.onopen = () => {
67
+ if (abortedRef.current) {
68
+ ws.close(1e3, "Effect cleaned up");
69
+ return;
70
+ }
65
71
  console.log("WebSocket connected");
66
72
  setIsConnected(true);
67
73
  setError(null);
@@ -69,12 +75,14 @@ function useWebSocket(config, externalWebSocket) {
69
75
  config.onConnectionChange?.(true);
70
76
  };
71
77
  ws.onerror = (event) => {
78
+ if (abortedRef.current) return;
72
79
  console.error("WebSocket error:", event);
73
80
  const err = new Error("WebSocket connection error");
74
81
  setError(err);
75
82
  config.onError?.(err);
76
83
  };
77
84
  ws.onclose = (event) => {
85
+ if (abortedRef.current) return;
78
86
  console.log("WebSocket closed:", event.code, event.reason);
79
87
  setIsConnected(false);
80
88
  config.onConnectionChange?.(false);
@@ -123,6 +131,7 @@ function useWebSocket(config, externalWebSocket) {
123
131
  );
124
132
  const reconnect = useCallback(() => {
125
133
  reconnectAttemptsRef.current = 0;
134
+ abortedRef.current = false;
126
135
  connect();
127
136
  }, [connect]);
128
137
  useEffect(() => {
@@ -132,8 +141,10 @@ function useWebSocket(config, externalWebSocket) {
132
141
  const cleanup = subscribeToMessage(externalWebSocket);
133
142
  return cleanup;
134
143
  }
144
+ abortedRef.current = false;
135
145
  connect();
136
146
  return () => {
147
+ abortedRef.current = true;
137
148
  if (reconnectTimeoutRef.current) {
138
149
  clearTimeout(reconnectTimeoutRef.current);
139
150
  }
@@ -157,7 +168,7 @@ function useWebSocket(config, externalWebSocket) {
157
168
  }
158
169
 
159
170
  // src/hooks/useMessages.ts
160
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
171
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
161
172
 
162
173
  // src/utils/api.ts
163
174
  import axios from "axios";
@@ -189,6 +200,7 @@ async function fetchMessages(baseUrl, params, headers) {
189
200
  }
190
201
 
191
202
  // src/hooks/useMessages.ts
203
+ var BOT_THINKING_TIMEOUT = 45e3;
192
204
  function useMessages(websocket, config) {
193
205
  const [messages, setMessages] = useState2([]);
194
206
  const [isLoading, setIsLoading] = useState2(false);
@@ -197,6 +209,7 @@ function useMessages(websocket, config) {
197
209
  const [hasMore, setHasMore] = useState2(true);
198
210
  const [isLoadingMore, setIsLoadingMore] = useState2(false);
199
211
  const [isBotThinking, setIsBotThinking] = useState2(false);
212
+ const botThinkingTimerRef = useRef2(null);
200
213
  const { httpApiUrl, conversationId, headers, onError, toast } = config;
201
214
  const headersWithApiKey = useMemo(
202
215
  () => ({
@@ -247,6 +260,10 @@ function useMessages(websocket, config) {
247
260
  });
248
261
  if (newMessage.senderType === "bot" || newMessage.senderType === "system") {
249
262
  setIsBotThinking(false);
263
+ if (botThinkingTimerRef.current) {
264
+ clearTimeout(botThinkingTimerRef.current);
265
+ botThinkingTimerRef.current = null;
266
+ }
250
267
  }
251
268
  onMessageReceived?.(newMessage);
252
269
  }
@@ -260,6 +277,10 @@ function useMessages(websocket, config) {
260
277
  });
261
278
  if (message.senderType === "customer") {
262
279
  setIsBotThinking(true);
280
+ if (botThinkingTimerRef.current) clearTimeout(botThinkingTimerRef.current);
281
+ botThinkingTimerRef.current = setTimeout(() => {
282
+ setIsBotThinking(false);
283
+ }, BOT_THINKING_TIMEOUT);
263
284
  }
264
285
  }, []);
265
286
  const updateMessageStatus = useCallback2((messageId, status) => {
@@ -303,6 +324,11 @@ function useMessages(websocket, config) {
303
324
  headersWithApiKey,
304
325
  onError
305
326
  ]);
327
+ useEffect2(() => {
328
+ return () => {
329
+ if (botThinkingTimerRef.current) clearTimeout(botThinkingTimerRef.current);
330
+ };
331
+ }, []);
306
332
  return {
307
333
  messages,
308
334
  addMessage,
@@ -451,7 +477,7 @@ function useTypingIndicator(websocket) {
451
477
  }
452
478
 
453
479
  // src/hooks/useResizableWidget.ts
454
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef2, useState as useState5 } from "react";
480
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
455
481
  var STORAGE_KEY = "xcelsior-chat-size";
456
482
  var EDGE_ZONE = 8;
457
483
  var CURSOR_MAP = {
@@ -519,10 +545,10 @@ function useResizableWidget({
519
545
  const [isResizing, setIsResizing] = useState5(false);
520
546
  const [isNearEdge, setIsNearEdge] = useState5(false);
521
547
  const [activeEdge, setActiveEdge] = useState5(null);
522
- const sizeRef = useRef2(size);
548
+ const sizeRef = useRef3(size);
523
549
  sizeRef.current = size;
524
- const dragRef = useRef2(null);
525
- const containerRef = useRef2(null);
550
+ const dragRef = useRef3(null);
551
+ const containerRef = useRef3(null);
526
552
  const clamp = useCallback3(
527
553
  (w, h) => {
528
554
  const mxW = Math.min(maxWidth, window.innerWidth - 24);
@@ -911,7 +937,7 @@ function ChatHeader({ agent, onClose, onMinimize, theme }) {
911
937
  }
912
938
 
913
939
  // src/components/MessageList.tsx
914
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef4 } from "react";
940
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5 } from "react";
915
941
 
916
942
  // src/components/Spinner.tsx
917
943
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -1476,7 +1502,7 @@ function MessageItem({
1476
1502
  }
1477
1503
 
1478
1504
  // src/components/ThinkingIndicator.tsx
1479
- import { useEffect as useEffect5, useRef as useRef3, useState as useState6 } from "react";
1505
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState6 } from "react";
1480
1506
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1481
1507
  var PHRASE_POOLS = {
1482
1508
  // ── Greetings & small talk ──
@@ -1663,7 +1689,7 @@ function ThinkingIndicator({
1663
1689
  const [phraseIndex, setPhraseIndex] = useState6(0);
1664
1690
  const [displayText, setDisplayText] = useState6("");
1665
1691
  const [isDeleting, setIsDeleting] = useState6(false);
1666
- const phrasesRef = useRef3(getShuffledPhrases(detectContext(lastUserMessage)));
1692
+ const phrasesRef = useRef4(getShuffledPhrases(detectContext(lastUserMessage)));
1667
1693
  useEffect5(() => {
1668
1694
  phrasesRef.current = getShuffledPhrases(detectContext(lastUserMessage));
1669
1695
  setPhraseIndex(0);
@@ -1782,13 +1808,13 @@ function MessageList({
1782
1808
  onQuickAction,
1783
1809
  isBotThinking = false
1784
1810
  }) {
1785
- const messagesEndRef = useRef4(null);
1786
- const containerRef = useRef4(null);
1787
- const prevLengthRef = useRef4(messages.length);
1788
- const loadMoreTriggerRef = useRef4(null);
1789
- const prevScrollHeightRef = useRef4(0);
1790
- const hasInitialScrolledRef = useRef4(false);
1791
- const isUserScrollingRef = useRef4(false);
1811
+ const messagesEndRef = useRef5(null);
1812
+ const containerRef = useRef5(null);
1813
+ const prevLengthRef = useRef5(messages.length);
1814
+ const loadMoreTriggerRef = useRef5(null);
1815
+ const prevScrollHeightRef = useRef5(0);
1816
+ const hasInitialScrolledRef = useRef5(false);
1817
+ const isUserScrollingRef = useRef5(false);
1792
1818
  const bgColor = theme?.background || "#00001a";
1793
1819
  const isLightTheme = (() => {
1794
1820
  if (!bgColor.startsWith("#")) return false;
@@ -2003,7 +2029,7 @@ function MessageList({
2003
2029
  }
2004
2030
 
2005
2031
  // src/components/ChatInput.tsx
2006
- import { useEffect as useEffect7, useRef as useRef5, useState as useState7 } from "react";
2032
+ import { useEffect as useEffect7, useRef as useRef6, useState as useState7 } from "react";
2007
2033
  import { createPortal } from "react-dom";
2008
2034
  import Picker from "@emoji-mart/react";
2009
2035
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -2029,13 +2055,13 @@ function ChatInput({
2029
2055
  const [emojiData, setEmojiData] = useState7();
2030
2056
  const [emojiPickerPosition, setEmojiPickerPosition] = useState7(null);
2031
2057
  const [isFocused, setIsFocused] = useState7(false);
2032
- const textAreaRef = useRef5(null);
2033
- const emojiPickerRef = useRef5(null);
2034
- const emojiButtonRef = useRef5(null);
2035
- const fileInputRef = useRef5(null);
2036
- const typingTimeoutRef = useRef5(null);
2037
- const startTypingTimeoutRef = useRef5(null);
2038
- const isTypingRef = useRef5(false);
2058
+ const textAreaRef = useRef6(null);
2059
+ const emojiPickerRef = useRef6(null);
2060
+ const emojiButtonRef = useRef6(null);
2061
+ const fileInputRef = useRef6(null);
2062
+ const typingTimeoutRef = useRef6(null);
2063
+ const startTypingTimeoutRef = useRef6(null);
2064
+ const isTypingRef = useRef6(false);
2039
2065
  const enableEmoji = config.enableEmoji ?? true;
2040
2066
  const enableFileUpload = config.enableFileUpload ?? true;
2041
2067
  const bgColor = config.theme?.background || "#00001a";
@@ -2578,7 +2604,6 @@ function ChatWidget({
2578
2604
  })();
2579
2605
  const positionClass = resolvedPosition === "left" ? "left-4" : "right-4";
2580
2606
  const containerStyle = isFullPage ? { backgroundColor: bgColor, color: textColor } : {
2581
- position: "relative",
2582
2607
  width,
2583
2608
  height,
2584
2609
  maxHeight: "calc(100vh - 100px)",
@@ -2764,7 +2789,7 @@ function ChatWidget({
2764
2789
  }
2765
2790
 
2766
2791
  // src/components/Chat.tsx
2767
- import { useCallback as useCallback8, useEffect as useEffect10, useState as useState11 } from "react";
2792
+ import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef8, useState as useState11 } from "react";
2768
2793
 
2769
2794
  // src/components/PreChatForm.tsx
2770
2795
  import { useState as useState8 } from "react";
@@ -3174,7 +3199,7 @@ function PreChatForm({
3174
3199
  }
3175
3200
 
3176
3201
  // src/hooks/useDraggablePosition.ts
3177
- import { useState as useState9, useCallback as useCallback6, useRef as useRef6, useEffect as useEffect9 } from "react";
3202
+ import { useState as useState9, useCallback as useCallback6, useRef as useRef7, useEffect as useEffect9 } from "react";
3178
3203
  var STORAGE_KEY2 = "xcelsior-chat-position";
3179
3204
  function getStoredPosition() {
3180
3205
  try {
@@ -3194,9 +3219,9 @@ function useDraggablePosition(configPosition = "auto") {
3194
3219
  const resolvedDefault = configPosition === "auto" ? getStoredPosition() : configPosition;
3195
3220
  const [position, setPosition] = useState9(resolvedDefault);
3196
3221
  const [isDragging, setIsDragging] = useState9(false);
3197
- const dragStartX = useRef6(0);
3198
- const fabRef = useRef6(null);
3199
- const hasShownHint = useRef6(false);
3222
+ const dragStartX = useRef7(0);
3223
+ const fabRef = useRef7(null);
3224
+ const hasShownHint = useRef7(false);
3200
3225
  const [showHint, setShowHint] = useState9(false);
3201
3226
  useEffect9(() => {
3202
3227
  try {
@@ -3295,59 +3320,36 @@ function Chat({
3295
3320
  const [userInfo, setUserInfo] = useState11(null);
3296
3321
  const [conversationId, setConversationId] = useState11("");
3297
3322
  const [isLoading, setIsLoading] = useState11(true);
3298
- const [isAnimating, setIsAnimating] = useState11(false);
3299
- const [showWidget, setShowWidget] = useState11(false);
3300
3323
  const identityMode = config.identityCollection || "progressive";
3301
3324
  const { position, isDragging, showHint, handlers } = useDraggablePosition(config.position);
3302
- const { currentState, setState: setStateRaw } = useChatWidgetState({
3325
+ const sessionInitializedRef = useRef8(false);
3326
+ const { currentState, setState } = useChatWidgetState({
3303
3327
  state,
3304
3328
  defaultState,
3305
3329
  onStateChange
3306
3330
  });
3307
- const setState = useCallback8(
3308
- (newState) => {
3309
- if (newState === "open" && currentState === "minimized") {
3310
- setShowWidget(true);
3311
- setIsAnimating(true);
3312
- setStateRaw(newState);
3313
- requestAnimationFrame(() => {
3314
- requestAnimationFrame(() => {
3315
- setIsAnimating(false);
3316
- });
3317
- });
3318
- } else if ((newState === "minimized" || newState === "closed") && currentState === "open") {
3319
- setIsAnimating(true);
3320
- setTimeout(() => {
3321
- setShowWidget(false);
3322
- setIsAnimating(false);
3323
- setStateRaw(newState);
3324
- }, 200);
3325
- } else {
3326
- setStateRaw(newState);
3327
- }
3328
- },
3329
- [currentState, setStateRaw]
3330
- );
3331
- useEffect10(() => {
3332
- if (currentState === "open") {
3333
- setShowWidget(true);
3334
- }
3335
- }, [currentState]);
3331
+ const configConversationId = config.conversationId;
3332
+ const configUserEmail = config.currentUser?.email;
3333
+ const configUserName = config.currentUser?.name;
3334
+ const configUserAvatar = config.currentUser?.avatar;
3335
+ const configUserStatus = config.currentUser?.status;
3336
3336
  useEffect10(() => {
3337
+ if (sessionInitializedRef.current) return;
3337
3338
  const initializeSession = () => {
3338
3339
  try {
3339
- if (config.currentUser?.email && config.currentUser?.name) {
3340
- const convId2 = config.conversationId || generateSessionId();
3340
+ if (configUserEmail && configUserName) {
3341
+ const convId2 = configConversationId || generateSessionId();
3341
3342
  const user = {
3342
- name: config.currentUser.name,
3343
- email: config.currentUser.email,
3344
- avatar: config.currentUser.avatar,
3343
+ name: configUserName,
3344
+ email: configUserEmail,
3345
+ avatar: configUserAvatar,
3345
3346
  type: "customer",
3346
- status: config.currentUser.status
3347
+ status: configUserStatus
3347
3348
  };
3348
3349
  setUserInfo(user);
3349
3350
  setConversationId(convId2);
3350
3351
  setIsLoading(false);
3352
+ sessionInitializedRef.current = true;
3351
3353
  return;
3352
3354
  }
3353
3355
  const storedDataJson = localStorage.getItem(`${storageKeyPrefix}_user`);
@@ -3364,23 +3366,26 @@ function Chat({
3364
3366
  setUserInfo(user);
3365
3367
  setConversationId(storedData.conversationId);
3366
3368
  setIsLoading(false);
3369
+ sessionInitializedRef.current = true;
3367
3370
  return;
3368
3371
  }
3369
3372
  }
3370
- const convId = config.conversationId || generateSessionId();
3373
+ const convId = configConversationId || generateSessionId();
3371
3374
  setConversationId(convId);
3372
3375
  if (identityMode === "progressive" || identityMode === "none") {
3373
3376
  setUserInfo(null);
3374
3377
  }
3378
+ sessionInitializedRef.current = true;
3375
3379
  } catch (error) {
3376
3380
  console.error("Error initializing chat session:", error);
3377
- setConversationId(config.conversationId || generateSessionId());
3381
+ setConversationId(configConversationId || generateSessionId());
3382
+ sessionInitializedRef.current = true;
3378
3383
  } finally {
3379
3384
  setIsLoading(false);
3380
3385
  }
3381
3386
  };
3382
3387
  initializeSession();
3383
- }, [config, storageKeyPrefix, identityMode]);
3388
+ }, [configConversationId, configUserEmail, configUserName, configUserAvatar, configUserStatus, storageKeyPrefix, identityMode]);
3384
3389
  const handlePreChatSubmit = useCallback8(
3385
3390
  (name, email) => {
3386
3391
  const convId = conversationId || generateSessionId();
@@ -3466,16 +3471,7 @@ function Chat({
3466
3471
  conversationId,
3467
3472
  currentUser: userInfo || void 0
3468
3473
  };
3469
- const widgetAnimationStyle = showWidget && !isAnimating ? {
3470
- opacity: 1,
3471
- transform: "translateY(0) scale(1)",
3472
- transition: "opacity 0.25s ease-out, transform 0.25s ease-out"
3473
- } : {
3474
- opacity: 0,
3475
- transform: "translateY(12px) scale(0.97)",
3476
- transition: "opacity 0.2s ease-in, transform 0.2s ease-in"
3477
- };
3478
- return /* @__PURE__ */ jsx11("div", { style: widgetAnimationStyle, children: /* @__PURE__ */ jsx11(
3474
+ return /* @__PURE__ */ jsx11(
3479
3475
  ChatWidget,
3480
3476
  {
3481
3477
  config: fullConfig,
@@ -3484,7 +3480,7 @@ function Chat({
3484
3480
  onMinimize: () => setState("minimized"),
3485
3481
  resolvedPosition: position
3486
3482
  }
3487
- ) });
3483
+ );
3488
3484
  }
3489
3485
 
3490
3486
  // src/components/TypingIndicator.tsx