@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.
- package/.storybook/preview.tsx +2 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +49 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +77 -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/src/hooks/useWebSocket.ts +18 -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/.storybook/preview.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import '@xcelsior/design-system/styles';
|
|
2
|
-
|
|
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
|
|
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
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
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 (
|
|
3391
|
-
const convId2 =
|
|
3391
|
+
if (configUserEmail && configUserName) {
|
|
3392
|
+
const convId2 = configConversationId || generateSessionId();
|
|
3392
3393
|
const user = {
|
|
3393
|
-
name:
|
|
3394
|
-
email:
|
|
3395
|
-
avatar:
|
|
3394
|
+
name: configUserName,
|
|
3395
|
+
email: configUserEmail,
|
|
3396
|
+
avatar: configUserAvatar,
|
|
3396
3397
|
type: "customer",
|
|
3397
|
-
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 =
|
|
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(
|
|
3432
|
+
setConversationId(configConversationId || generateSessionId());
|
|
3433
|
+
sessionInitializedRef.current = true;
|
|
3429
3434
|
} finally {
|
|
3430
3435
|
setIsLoading(false);
|
|
3431
3436
|
}
|
|
3432
3437
|
};
|
|
3433
3438
|
initializeSession();
|
|
3434
|
-
}, [
|
|
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
|
-
|
|
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
|