@axos-web-dev/shared-components 2.2.29 → 2.2.30

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.
@@ -3,6 +3,7 @@ import { default as React } from 'react';
3
3
 
4
4
  interface MessageResponse extends BaseMessageResponse {
5
5
  to_agent?: HumanAgent;
6
+ pending?: boolean;
6
7
  }
7
8
  export interface ChatBubble {
8
9
  id: string | number;
@@ -18,7 +18,6 @@ import { useOpenChat } from "./store/chat.js";
18
18
  import { ThankYouMessage } from "./ThankYouMessage.js";
19
19
  const ChatWindow = ({
20
20
  messages,
21
- status = "connected",
22
21
  onSend,
23
22
  inputDisabled = false,
24
23
  onClose,
@@ -43,7 +42,6 @@ const ChatWindow = ({
43
42
  showThankyouMessage,
44
43
  toggleThankyouMessage,
45
44
  hasEscalated,
46
- isBlockedInput,
47
45
  isOpen,
48
46
  hasOpenedOnce,
49
47
  chatStatus,
@@ -65,7 +63,6 @@ const ChatWindow = ({
65
63
  const handleSend = (e) => {
66
64
  e.preventDefault();
67
65
  const cleaned = cleanInput(input);
68
- if (isBlockedInput) return;
69
66
  if (cleaned != "") {
70
67
  onSend(cleaned);
71
68
  setInput("");
@@ -460,7 +457,7 @@ const ChatWindow = ({
460
457
  }
461
458
  ) }) : null,
462
459
  /* @__PURE__ */ jsxs("div", { className: clsx(messagesContainerStyle), children: [
463
- (status === "idle" && messages.length == 0 || messages.length == 0) && /* @__PURE__ */ jsx(
460
+ !messages.some((msg) => !msg.pending) && /* @__PURE__ */ jsx(
464
461
  "div",
465
462
  {
466
463
  className: clsx(),
@@ -525,9 +522,7 @@ const ChatWindow = ({
525
522
  /* @__PURE__ */ jsx(
526
523
  "div",
527
524
  {
528
- className: clsx(
529
- focused && !inputDisabled && !isBlockedInput ? focusAnimation : ""
530
- ),
525
+ className: clsx(focused && !inputDisabled ? focusAnimation : ""),
531
526
  style: {
532
527
  position: "absolute",
533
528
  left: "10%",
@@ -544,7 +539,7 @@ const ChatWindow = ({
544
539
  style: {
545
540
  display: "flex",
546
541
  padding: "12px 16px",
547
- background: isBlockedInput || inputDisabled || status !== "connected" || messages.length == 0 || escalationDeflected ? "light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3))" : "#ffffff",
542
+ background: inputDisabled || escalationDeflected ? "light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3))" : "#ffffff",
548
543
  borderRadius: 12
549
544
  },
550
545
  children: [
@@ -568,7 +563,7 @@ const ChatWindow = ({
568
563
  "aria-label": "Ask anything...",
569
564
  className: clsx(inputStyle, autoResize),
570
565
  autoFocus: true,
571
- disabled: isBlockedInput || inputDisabled || status !== "connected" || messages.length == 0 || escalationDeflected,
566
+ disabled: inputDisabled || escalationDeflected,
572
567
  rows: 1
573
568
  }
574
569
  ),
@@ -582,7 +577,7 @@ const ChatWindow = ({
582
577
  type: "submit",
583
578
  title: "Send message",
584
579
  "aria-label": "Send message",
585
- disabled: isBlockedInput || inputDisabled || status !== "connected" || !input.trim() || messages.length == 0 || escalationDeflected,
580
+ disabled: inputDisabled || !input.trim() || escalationDeflected,
586
581
  children: /* @__PURE__ */ jsx(
587
582
  "svg",
588
583
  {
@@ -40,17 +40,34 @@ const Chatbot = ({
40
40
  hideDisplayThankyouMessage
41
41
  } = useOpenChat();
42
42
  const isVisible = usePageVisibility();
43
- const { addMessage, addMessages, clearMessages, messages } = useMessages();
43
+ const {
44
+ addMessage,
45
+ addMessages,
46
+ clearMessages,
47
+ messages,
48
+ addPendingMessage,
49
+ resolvePendingMessage
50
+ } = useMessages();
44
51
  const clientRef = useRef(null);
45
52
  const menuRef = useRef(null);
46
53
  const isMountedRef = useRef(false);
47
54
  const chatRef = useRef(null);
48
55
  const chatLoading = useRef(false);
49
56
  const agent_virtual = useRef(null);
57
+ const messageQueueRef = useRef([]);
58
+ const sendInFlightRef = useRef(false);
59
+ const flushingRef = useRef(false);
60
+ const statusRef = useRef("idle");
50
61
  const [status, setStatus] = useState("idle");
51
62
  const [menusLoaded, setMenusLoaded] = useState(false);
52
63
  const [isTyping, setIsTyping] = useState(false);
53
64
  const [scalationStarted, setScalationStarted] = useState(false);
65
+ useEffect(() => {
66
+ statusRef.current = status;
67
+ if (status === "connected") {
68
+ flushQueuedMessages();
69
+ }
70
+ }, [status]);
54
71
  useEffect(() => {
55
72
  if (messages.length === 0) return;
56
73
  const hasScalation = messages.some(
@@ -67,6 +84,9 @@ const Chatbot = ({
67
84
  ["axos", 1],
68
85
  ["ufb", 3]
69
86
  ]);
87
+ const hasRealReply = () => useMessages.getState().messages.some(
88
+ (m) => !m.pending && m.$userType !== "end_user" && m.type !== "noti" && m.$sid !== "typing-1"
89
+ );
70
90
  const typingMessage = {
71
91
  $sid: "typing-1",
72
92
  type: "system",
@@ -107,7 +127,7 @@ const Chatbot = ({
107
127
  menuRef.current = await clientRef.current.getMenus();
108
128
  setMenusLoaded(true);
109
129
  console.log("menus:", menuRef.current);
110
- if (menuRef.current !== null) {
130
+ if (menuRef.current !== null && useOpenChat.getState().isOpen) {
111
131
  await startChat("onReady");
112
132
  }
113
133
  }
@@ -121,10 +141,11 @@ const Chatbot = ({
121
141
  const onChatMessageHandler = async (message) => {
122
142
  console.log("Received message:", message);
123
143
  const { event, $userType } = message;
124
- if (["system", "virtual_agent", "user"].includes($userType) && event === void 0) {
144
+ if (["virtual_agent", "user"].includes($userType) && event === void 0) {
125
145
  addMessage(message);
126
146
  if (!hasEscalated) {
127
147
  unblockInput?.();
148
+ flushQueuedMessages();
128
149
  }
129
150
  return;
130
151
  }
@@ -205,6 +226,7 @@ const Chatbot = ({
205
226
  startEscalation?.();
206
227
  }
207
228
  }
229
+ flushQueuedMessages();
208
230
  } catch (error) {
209
231
  console.error("Error fetching messages on chat connected:", error);
210
232
  }
@@ -226,7 +248,7 @@ const Chatbot = ({
226
248
  const deregisterEventHandlers = () => {
227
249
  clientRef.current?.off("ready", onReadyHandler);
228
250
  clientRef.current?.off("authenticated", onAuthenticatedHandler);
229
- clientRef.current?.off("chat.ongoing", onDismissedHandler);
251
+ clientRef.current?.off("chat.ongoing", onChatOngoingHandler);
230
252
  clientRef.current?.off("chat.message", onChatMessageHandler);
231
253
  clientRef.current?.off("chat.typingStarted", onChatTypingStartedHandler);
232
254
  clientRef.current?.off("chat.typingEnded", onChatTypingEndedHandler);
@@ -340,6 +362,41 @@ const Chatbot = ({
340
362
  setChatStarted();
341
363
  }
342
364
  };
365
+ const sendNow = async (msg) => {
366
+ sendInFlightRef.current = true;
367
+ let success = true;
368
+ try {
369
+ await clientRef.current?.sendTextMessage(msg);
370
+ } catch (error) {
371
+ console.log(error);
372
+ success = false;
373
+ } finally {
374
+ sendInFlightRef.current = false;
375
+ if (!hasEscalated) {
376
+ blockInput?.();
377
+ }
378
+ }
379
+ return success;
380
+ };
381
+ const flushQueuedMessages = async () => {
382
+ if (flushingRef.current) return;
383
+ if (messageQueueRef.current.length === 0) return;
384
+ if (!clientRef.current || statusRef.current !== "connected") return;
385
+ if (!hasRealReply()) return;
386
+ flushingRef.current = true;
387
+ try {
388
+ while (messageQueueRef.current.length > 0 && !useOpenChat.getState().isBlockedInput && !sendInFlightRef.current) {
389
+ const next = messageQueueRef.current[0];
390
+ const success = await sendNow(next.text);
391
+ messageQueueRef.current.shift();
392
+ if (!success) {
393
+ resolvePendingMessage(next.id);
394
+ }
395
+ }
396
+ } finally {
397
+ flushingRef.current = false;
398
+ }
399
+ };
343
400
  const onSendMessage = async (msg) => {
344
401
  const clientChatId = clientRef.current?.chat?.id;
345
402
  const refChatId = chatRef.current?.id;
@@ -351,15 +408,16 @@ const Chatbot = ({
351
408
  "| chatRef.id:",
352
409
  refChatId
353
410
  );
354
- try {
355
- await clientRef.current?.sendTextMessage(msg);
356
- } catch (error) {
357
- console.log(error);
358
- } finally {
359
- if (!hasEscalated) {
360
- blockInput?.();
361
- }
411
+ const notReady = !clientRef.current || statusRef.current !== "connected";
412
+ const isThinking = useOpenChat.getState().isBlockedInput;
413
+ const noReplyYet = !hasRealReply();
414
+ if (notReady || isThinking || sendInFlightRef.current || noReplyYet) {
415
+ const id = `pending-${Date.now()}-${Math.random().toString(36).slice(2)}`;
416
+ messageQueueRef.current.push({ id, text: msg });
417
+ addPendingMessage(id, msg);
418
+ return;
362
419
  }
420
+ await sendNow(msg);
363
421
  };
364
422
  const onEndChat = async () => {
365
423
  console.log(`Ending chat [end chat]`);
@@ -373,6 +431,9 @@ const Chatbot = ({
373
431
  clearMessages();
374
432
  chatRef.current = null;
375
433
  resetChatStarted();
434
+ messageQueueRef.current = [];
435
+ sendInFlightRef.current = false;
436
+ flushingRef.current = false;
376
437
  console.log("Chat ended");
377
438
  endEscalation?.();
378
439
  } finally {
@@ -399,6 +460,8 @@ const Chatbot = ({
399
460
  console.log("Chatbot unmounted");
400
461
  deregisterEventHandlers();
401
462
  chatLoading.current = false;
463
+ messageQueueRef.current = [];
464
+ sendInFlightRef.current = false;
402
465
  reset();
403
466
  resetChatStarted();
404
467
  setStatus("idle");
@@ -443,36 +506,6 @@ const Chatbot = ({
443
506
  }
444
507
  }
445
508
  }, [isOpen, isVisible]);
446
- useMount(() => {
447
- const hasParams = () => {
448
- return window.location.search.includes("utm_");
449
- };
450
- const tryStart = () => {
451
- if (hasParams()) {
452
- startChat("utm-ready");
453
- return true;
454
- }
455
- return false;
456
- };
457
- if (tryStart()) return;
458
- const original = history.replaceState;
459
- history.replaceState = function(...args) {
460
- original.apply(this, args);
461
- if (tryStart()) {
462
- history.replaceState = original;
463
- }
464
- };
465
- const interval = setInterval(() => {
466
- if (tryStart()) {
467
- clearInterval(interval);
468
- history.replaceState = original;
469
- }
470
- }, 100);
471
- return () => {
472
- clearInterval(interval);
473
- history.replaceState = original;
474
- };
475
- });
476
509
  return menusLoaded && /* @__PURE__ */ jsxs(
477
510
  "div",
478
511
  {
@@ -3,6 +3,7 @@ import { MessageResponse as BaseMessageResponse, HumanAgent, VirtualAgent } from
3
3
 
4
4
  interface MessageResponse extends BaseMessageResponse {
5
5
  to_agent?: HumanAgent;
6
+ pending?: boolean;
6
7
  }
7
8
  interface ChatbotMessageProps {
8
9
  msg: MessageResponse;
@@ -108,6 +108,7 @@ const ChatbotMessage = ({
108
108
  messageStyle,
109
109
  msg.$userType == "end_user" ? user_msg : agent_msg
110
110
  ),
111
+ style: msg.pending ? { opacity: 0.65 } : void 0,
111
112
  children: /* @__PURE__ */ jsxs(
112
113
  "div",
113
114
  {
@@ -253,7 +254,7 @@ const ChatbotMessage = ({
253
254
  textTransform: "capitalize",
254
255
  fontFamily: "inherit"
255
256
  },
256
- children: timeText
257
+ children: msg.pending ? "Sending..." : timeText
257
258
  }
258
259
  )
259
260
  ]
@@ -1,12 +1,17 @@
1
1
  import { MessageResponse } from '@ujet/websdk-headless';
2
2
 
3
+ export type PendingMessage = MessageResponse & {
4
+ pending?: boolean;
5
+ };
3
6
  interface MessageStore {
4
- messages: Array<MessageResponse>;
7
+ messages: Array<PendingMessage>;
5
8
  addMessage: (message: MessageResponse) => void;
6
9
  removeMessage: (id: string) => void;
7
10
  addMessages: (newMessages: MessageResponse[]) => void;
8
11
  clearMessages: () => void;
9
12
  isEscalated: boolean;
13
+ addPendingMessage: (id: string, text: string) => void;
14
+ resolvePendingMessage: (id: string) => void;
10
15
  }
11
16
  export interface Message {
12
17
  id: string;
@@ -3,18 +3,77 @@ const useMessages = create((set, get) => ({
3
3
  messages: [],
4
4
  addMessage: (message) => set((state) => {
5
5
  const cleaned = state.messages.filter((m) => m.$sid !== "typing-1");
6
- return { messages: [...cleaned, message] };
6
+ const isPending = message.pending;
7
+ if (!isPending && message.$userType === "end_user") {
8
+ const pendingIdx = cleaned.findIndex(
9
+ (m) => m.pending && m.content === message.content
10
+ );
11
+ if (pendingIdx !== -1) {
12
+ return {
13
+ messages: [
14
+ ...cleaned.slice(0, pendingIdx),
15
+ message,
16
+ ...cleaned.slice(pendingIdx + 1)
17
+ ]
18
+ };
19
+ }
20
+ return { messages: [...cleaned, message] };
21
+ }
22
+ const insertBeforeIdx = !isPending ? cleaned.findIndex((m) => m.pending) : -1;
23
+ if (insertBeforeIdx === -1) {
24
+ return { messages: [...cleaned, message] };
25
+ }
26
+ return {
27
+ messages: [
28
+ ...cleaned.slice(0, insertBeforeIdx),
29
+ message,
30
+ ...cleaned.slice(insertBeforeIdx)
31
+ ]
32
+ };
7
33
  }),
8
34
  removeMessage: (id) => set((state) => ({
9
35
  messages: state.messages.filter(
10
36
  (msg) => "$sid" in msg && msg.$sid !== id
11
37
  )
12
38
  })),
13
- addMessages: (newMessages) => set((state) => ({ messages: [...state.messages, ...newMessages] })),
39
+ addMessages: (newMessages) => set((state) => {
40
+ const pendingIdx = state.messages.findIndex(
41
+ (m) => m.pending
42
+ );
43
+ if (pendingIdx === -1) {
44
+ return { messages: [...state.messages, ...newMessages] };
45
+ }
46
+ return {
47
+ messages: [
48
+ ...state.messages.slice(0, pendingIdx),
49
+ ...newMessages,
50
+ ...state.messages.slice(pendingIdx)
51
+ ]
52
+ };
53
+ }),
14
54
  clearMessages: () => set({ messages: [] }),
15
55
  isEscalated: get()?.messages?.some(
16
56
  (msg) => ["escalationAccepted", "escalationStarted"].includes(msg.event)
17
- )
57
+ ),
58
+ addPendingMessage: (id, text) => set((state) => ({
59
+ messages: [
60
+ ...state.messages,
61
+ {
62
+ $sid: id,
63
+ type: "text",
64
+ content: text,
65
+ sender: { id: "end_user", type: "end_user" },
66
+ $timestamp: /* @__PURE__ */ new Date(),
67
+ $userType: "end_user",
68
+ $index: Date.now(),
69
+ $userId: 0,
70
+ pending: true
71
+ }
72
+ ]
73
+ })),
74
+ resolvePendingMessage: (id) => set((state) => ({
75
+ messages: state.messages.filter((msg) => msg.$sid !== id)
76
+ }))
18
77
  }));
19
78
  export {
20
79
  useMessages
@@ -293,6 +293,12 @@ main > div:nth-last-child(2) > ._1m7m2a0:not(._1m7m2ax) {
293
293
  .page_body_contents > div:has(._1m7m2ax._1m7m2a4) + div:has(._1m7m2ax._1m7m2a4) {
294
294
  padding-top: 0;
295
295
  }
296
+ .page_body_contents > div:has(.stacked) {
297
+ padding-bottom: 40px;
298
+ }
299
+ .page_body_contents > div:has(> ._1m7m2ax) {
300
+ padding-block: 56px;
301
+ }
296
302
  .page_body_contents > div:has(._1m7m2ax) + div:has(._1m7m2ax) {
297
303
  margin-top: 0;
298
304
  }
@@ -459,6 +465,9 @@ main > div:nth-last-child(2) > ._1m7m2a0:not(._1m7m2ax) {
459
465
  .stacked:first-child {
460
466
  padding-top: 42px;
461
467
  }
468
+ .page_body_contents > div:has(> ._1m7m2ax) {
469
+ padding-block: 42px;
470
+ }
462
471
  }
463
472
  @media screen and (max-width: 768px) {
464
473
  ._1m7m2av {
@@ -476,6 +485,9 @@ main > div:nth-last-child(2) > ._1m7m2a0:not(._1m7m2ax) {
476
485
  .stacked:first-child {
477
486
  padding-top: 2rem;
478
487
  }
488
+ .page_body_contents > div:has(> ._1m7m2ax) {
489
+ padding-block: 2rem;
490
+ }
479
491
  }
480
492
  @media screen and (max-width: 327px) {
481
493
  ._1m7m2av {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@axos-web-dev/shared-components",
3
3
  "description": "Axos shared components library for web.",
4
- "version": "2.2.29",
4
+ "version": "2.2.30",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
7
7
  "module": "dist/main.js",