@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.
- package/.storybook/preview.tsx +2 -1
- package/dist/index.js +38 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +66 -81
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/components/Chat.tsx +35 -74
- package/src/components/ChatWidget.tsx +0 -1
- package/src/hooks/useMessages.ts +22 -1
- package/storybook-static/assets/Chat.stories-BkbpOOSG.js +830 -0
- package/storybook-static/assets/{Color-YHDXOIA2-BMnd3YrF.js → Color-YHDXOIA2-CSuNIR0a.js} +1 -1
- package/storybook-static/assets/{DocsRenderer-CFRXHY34-i_W8iCu9.js → DocsRenderer-CFRXHY34-dpuOKTQp.js} +3 -3
- package/storybook-static/assets/{MessageItem-DAaKZ9s9.js → MessageItem-Dlb6dSKL.js} +9 -9
- package/storybook-static/assets/MessageItem.stories-CsxqSqu-.js +422 -0
- package/storybook-static/assets/{entry-preview-oDnntGcx.js → entry-preview-C_-WO6GJ.js} +1 -1
- package/storybook-static/assets/{iframe-CGBtu2Se.js → iframe-BXTccXxS.js} +2 -2
- package/storybook-static/assets/preview-B8y-wc-n.css +1 -0
- package/storybook-static/assets/preview-CC4t7T7W.js +1 -0
- package/storybook-static/assets/{preview-BRpahs9B.js → preview-Cyx3pE7Q.js} +2 -2
- package/storybook-static/iframe.html +1 -1
- package/storybook-static/index.json +1 -1
- package/storybook-static/project.json +1 -1
- package/tsconfig.json +4 -0
- package/storybook-static/assets/Chat.stories-J_Yp51wU.js +0 -803
- package/storybook-static/assets/MessageItem.stories-Ckr1_scc.js +0 -255
- package/storybook-static/assets/ToastContext-Bty1K7ya.js +0 -1
- package/storybook-static/assets/preview-DUOvJmsz.js +0 -1
- 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
|
+
"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",
|
package/src/components/Chat.tsx
CHANGED
|
@@ -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
|
|
53
|
+
const { currentState, setState } = useChatWidgetState({
|
|
55
54
|
state,
|
|
56
55
|
defaultState,
|
|
57
56
|
onStateChange,
|
|
58
57
|
});
|
|
59
58
|
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 (
|
|
104
|
-
const convId =
|
|
73
|
+
if (configUserEmail && configUserName) {
|
|
74
|
+
const convId = configConversationId || generateSessionId();
|
|
105
75
|
const user: IUser = {
|
|
106
|
-
name:
|
|
107
|
-
email:
|
|
108
|
-
avatar:
|
|
76
|
+
name: configUserName,
|
|
77
|
+
email: configUserEmail,
|
|
78
|
+
avatar: configUserAvatar,
|
|
109
79
|
type: 'customer',
|
|
110
|
-
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 =
|
|
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(
|
|
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
|
-
}, [
|
|
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
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
}
|
package/src/hooks/useMessages.ts
CHANGED
|
@@ -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,
|