@xcelsior/ui-chat 2.0.3 → 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 (28) hide show
  1. package/.storybook/preview.tsx +2 -1
  2. package/dist/index.js +38 -53
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +66 -81
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +3 -2
  7. package/src/components/Chat.tsx +35 -74
  8. package/src/components/ChatWidget.tsx +0 -1
  9. package/src/hooks/useMessages.ts +22 -1
  10. package/storybook-static/assets/Chat.stories-BkbpOOSG.js +830 -0
  11. package/storybook-static/assets/{Color-YHDXOIA2-BMnd3YrF.js → Color-YHDXOIA2-CSuNIR0a.js} +1 -1
  12. package/storybook-static/assets/{DocsRenderer-CFRXHY34-i_W8iCu9.js → DocsRenderer-CFRXHY34-dpuOKTQp.js} +3 -3
  13. package/storybook-static/assets/{MessageItem-DAaKZ9s9.js → MessageItem-Dlb6dSKL.js} +9 -9
  14. package/storybook-static/assets/MessageItem.stories-CsxqSqu-.js +422 -0
  15. package/storybook-static/assets/{entry-preview-oDnntGcx.js → entry-preview-C_-WO6GJ.js} +1 -1
  16. package/storybook-static/assets/{iframe-CGBtu2Se.js → iframe-BXTccXxS.js} +2 -2
  17. package/storybook-static/assets/preview-B8y-wc-n.css +1 -0
  18. package/storybook-static/assets/preview-CC4t7T7W.js +1 -0
  19. package/storybook-static/assets/{preview-BRpahs9B.js → preview-Cyx3pE7Q.js} +2 -2
  20. package/storybook-static/iframe.html +1 -1
  21. package/storybook-static/index.json +1 -1
  22. package/storybook-static/project.json +1 -1
  23. package/tsconfig.json +4 -0
  24. package/storybook-static/assets/Chat.stories-J_Yp51wU.js +0 -803
  25. package/storybook-static/assets/MessageItem.stories-Ckr1_scc.js +0 -255
  26. package/storybook-static/assets/ToastContext-Bty1K7ya.js +0 -1
  27. package/storybook-static/assets/preview-DUOvJmsz.js +0 -1
  28. package/storybook-static/assets/preview-DcGwT3kv.css +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/ui-chat",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "license": "MIT",
@@ -21,7 +21,8 @@
21
21
  "storybook": "^8.6.14",
22
22
  "@tailwindcss/postcss": "^4.1.13",
23
23
  "tsup": "^8.0.2",
24
- "typescript": "^5.3.3"
24
+ "typescript": "^5.3.3",
25
+ "@xcelsior/design-system": "1.0.10"
25
26
  },
26
27
  "peerDependencies": {
27
28
  "tailwindcss": "^4",
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { ChatWidget } from './ChatWidget';
3
3
  import { PreChatForm } from './PreChatForm';
4
4
  import { ChatBubbleIcon } from './BrandIcons';
@@ -45,73 +45,44 @@ export function Chat({
45
45
  const [userInfo, setUserInfo] = useState<IUser | null>(null);
46
46
  const [conversationId, setConversationId] = useState<string>('');
47
47
  const [isLoading, setIsLoading] = useState(true);
48
- const [isAnimating, setIsAnimating] = useState(false);
49
- const [showWidget, setShowWidget] = useState(false);
50
48
 
51
49
  const identityMode = config.identityCollection || 'progressive';
52
50
  const { position, isDragging, showHint, handlers } = useDraggablePosition(config.position);
51
+ const sessionInitializedRef = useRef(false);
53
52
 
54
- const { currentState, setState: setStateRaw } = useChatWidgetState({
53
+ const { currentState, setState } = useChatWidgetState({
55
54
  state,
56
55
  defaultState,
57
56
  onStateChange,
58
57
  });
59
58
 
60
- // Wrap setState with animation handling
61
- const setState = useCallback(
62
- (newState: ChatWidgetState) => {
63
- if (newState === 'open' && currentState === 'minimized') {
64
- // Opening: show widget immediately, trigger animation
65
- setShowWidget(true);
66
- setIsAnimating(true);
67
- setStateRaw(newState);
68
- // Let the animation class apply on next frame
69
- requestAnimationFrame(() => {
70
- requestAnimationFrame(() => {
71
- setIsAnimating(false);
72
- });
73
- });
74
- } else if (
75
- (newState === 'minimized' || newState === 'closed') &&
76
- currentState === 'open'
77
- ) {
78
- // Closing: animate out, then change state
79
- setIsAnimating(true);
80
- setTimeout(() => {
81
- setShowWidget(false);
82
- setIsAnimating(false);
83
- setStateRaw(newState);
84
- }, 200);
85
- } else {
86
- setStateRaw(newState);
87
- }
88
- },
89
- [currentState, setStateRaw]
90
- );
91
-
92
- // Sync showWidget with state
93
- useEffect(() => {
94
- if (currentState === 'open') {
95
- setShowWidget(true);
96
- }
97
- }, [currentState]);
59
+ // Extract primitives from config for stable dependency array
60
+ const configConversationId = config.conversationId;
61
+ const configUserEmail = config.currentUser?.email;
62
+ const configUserName = config.currentUser?.name;
63
+ const configUserAvatar = config.currentUser?.avatar;
64
+ const configUserStatus = config.currentUser?.status;
98
65
 
99
66
  // Initialize session
100
67
  useEffect(() => {
68
+ // Guard: only run once unless identity actually changes
69
+ if (sessionInitializedRef.current) return;
70
+
101
71
  const initializeSession = () => {
102
72
  try {
103
- if (config.currentUser?.email && config.currentUser?.name) {
104
- const convId = config.conversationId || generateSessionId();
73
+ if (configUserEmail && configUserName) {
74
+ const convId = configConversationId || generateSessionId();
105
75
  const user: IUser = {
106
- name: config.currentUser.name,
107
- email: config.currentUser.email,
108
- avatar: config.currentUser.avatar,
76
+ name: configUserName,
77
+ email: configUserEmail,
78
+ avatar: configUserAvatar,
109
79
  type: 'customer',
110
- status: config.currentUser.status,
80
+ status: configUserStatus,
111
81
  };
112
82
  setUserInfo(user);
113
83
  setConversationId(convId);
114
84
  setIsLoading(false);
85
+ sessionInitializedRef.current = true;
115
86
  return;
116
87
  }
117
88
 
@@ -130,26 +101,29 @@ export function Chat({
130
101
  setUserInfo(user);
131
102
  setConversationId(storedData.conversationId);
132
103
  setIsLoading(false);
104
+ sessionInitializedRef.current = true;
133
105
  return;
134
106
  }
135
107
  }
136
108
 
137
- const convId = config.conversationId || generateSessionId();
109
+ const convId = configConversationId || generateSessionId();
138
110
  setConversationId(convId);
139
111
 
140
112
  if (identityMode === 'progressive' || identityMode === 'none') {
141
113
  setUserInfo(null);
142
114
  }
115
+ sessionInitializedRef.current = true;
143
116
  } catch (error) {
144
117
  console.error('Error initializing chat session:', error);
145
- setConversationId(config.conversationId || generateSessionId());
118
+ setConversationId(configConversationId || generateSessionId());
119
+ sessionInitializedRef.current = true;
146
120
  } finally {
147
121
  setIsLoading(false);
148
122
  }
149
123
  };
150
124
 
151
125
  initializeSession();
152
- }, [config, storageKeyPrefix, identityMode]);
126
+ }, [configConversationId, configUserEmail, configUserName, configUserAvatar, configUserStatus, storageKeyPrefix, identityMode]);
153
127
 
154
128
  const handlePreChatSubmit = useCallback(
155
129
  (name: string, email: string) => {
@@ -245,29 +219,16 @@ export function Chat({
245
219
  currentUser: userInfo || undefined,
246
220
  };
247
221
 
248
- // Animation styles for open/close
249
- const widgetAnimationStyle: React.CSSProperties =
250
- showWidget && !isAnimating
251
- ? {
252
- opacity: 1,
253
- transform: 'translateY(0) scale(1)',
254
- transition: 'opacity 0.25s ease-out, transform 0.25s ease-out',
255
- }
256
- : {
257
- opacity: 0,
258
- transform: 'translateY(12px) scale(0.97)',
259
- transition: 'opacity 0.2s ease-in, transform 0.2s ease-in',
260
- };
261
-
222
+ // Render ChatWidget directly — no wrapper div.
223
+ // A wrapper with CSS `transform` (even identity) creates a new containing block,
224
+ // which breaks `position: fixed` on the ChatWidget.
262
225
  return (
263
- <div style={widgetAnimationStyle}>
264
- <ChatWidget
265
- config={fullConfig}
266
- className={className}
267
- onClose={() => setState('closed')}
268
- onMinimize={() => setState('minimized')}
269
- resolvedPosition={position}
270
- />
271
- </div>
226
+ <ChatWidget
227
+ config={fullConfig}
228
+ className={className}
229
+ onClose={() => setState('closed')}
230
+ onMinimize={() => setState('minimized')}
231
+ resolvedPosition={position}
232
+ />
272
233
  );
273
234
  }
@@ -147,7 +147,6 @@ export function ChatWidget({
147
147
  const containerStyle: React.CSSProperties = isFullPage
148
148
  ? { backgroundColor: bgColor, color: textColor }
149
149
  : {
150
- position: 'relative',
151
150
  width,
152
151
  height,
153
152
  maxHeight: 'calc(100vh - 100px)',
@@ -1,8 +1,11 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import type { IMessage, IChatConfig, UseMessagesReturn } from '../types';
3
3
  import type { UseWebSocketReturn } from './useWebSocket';
4
4
  import { fetchMessages } from '../utils/api';
5
5
 
6
+ /** Max time (ms) to show the bot thinking indicator before auto-clearing */
7
+ const BOT_THINKING_TIMEOUT = 45_000;
8
+
6
9
  export function useMessages(websocket: UseWebSocketReturn, config: IChatConfig): UseMessagesReturn {
7
10
  const [messages, setMessages] = useState<IMessage[]>([]);
8
11
  const [isLoading, setIsLoading] = useState(false);
@@ -11,6 +14,7 @@ export function useMessages(websocket: UseWebSocketReturn, config: IChatConfig):
11
14
  const [hasMore, setHasMore] = useState(true);
12
15
  const [isLoadingMore, setIsLoadingMore] = useState(false);
13
16
  const [isBotThinking, setIsBotThinking] = useState(false);
17
+ const botThinkingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
14
18
 
15
19
  // Extract stable references from config
16
20
  const { httpApiUrl, conversationId, headers, onError, toast } = config;
@@ -81,6 +85,10 @@ export function useMessages(websocket: UseWebSocketReturn, config: IChatConfig):
81
85
  // Clear bot thinking state when bot or system message arrives
82
86
  if (newMessage.senderType === 'bot' || newMessage.senderType === 'system') {
83
87
  setIsBotThinking(false);
88
+ if (botThinkingTimerRef.current) {
89
+ clearTimeout(botThinkingTimerRef.current);
90
+ botThinkingTimerRef.current = null;
91
+ }
84
92
  }
85
93
 
86
94
  // Notify parent component about new message
@@ -99,6 +107,12 @@ export function useMessages(websocket: UseWebSocketReturn, config: IChatConfig):
99
107
  // Show bot thinking indicator immediately when customer sends a message
100
108
  if (message.senderType === 'customer') {
101
109
  setIsBotThinking(true);
110
+
111
+ // Safety timeout — clear thinking state if no bot response arrives
112
+ if (botThinkingTimerRef.current) clearTimeout(botThinkingTimerRef.current);
113
+ botThinkingTimerRef.current = setTimeout(() => {
114
+ setIsBotThinking(false);
115
+ }, BOT_THINKING_TIMEOUT);
102
116
  }
103
117
  }, []);
104
118
 
@@ -149,6 +163,13 @@ export function useMessages(websocket: UseWebSocketReturn, config: IChatConfig):
149
163
  onError,
150
164
  ]);
151
165
 
166
+ // Clean up timer on unmount
167
+ useEffect(() => {
168
+ return () => {
169
+ if (botThinkingTimerRef.current) clearTimeout(botThinkingTimerRef.current);
170
+ };
171
+ }, []);
172
+
152
173
  return {
153
174
  messages,
154
175
  addMessage,