@helpway/react 0.3.4 → 0.4.1

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/dist/index.js CHANGED
@@ -221,8 +221,10 @@ function createWidgetStore() {
221
221
  typingUsers: /* @__PURE__ */ new Map(),
222
222
  aiProcessing: /* @__PURE__ */ new Map(),
223
223
  isOpen: false,
224
+ isDismissed: false,
224
225
  view: "home",
225
226
  incomingPreview: null,
227
+ newConversationPrefill: null,
226
228
  contentCache: /* @__PURE__ */ new Map(),
227
229
  // ── Lifecycle ────────────────────────────────────
228
230
  setReady(visitor, website, agentConfig) {
@@ -377,12 +379,17 @@ function createWidgetStore() {
377
379
  set((s) => ({
378
380
  isOpen: open,
379
381
  incomingPreview: open ? null : s.incomingPreview
380
- // dismiss preview when opening
381
382
  }));
382
383
  },
384
+ setDismissed(dismissed) {
385
+ set({ isDismissed: dismissed });
386
+ },
383
387
  setView(view) {
384
388
  set({ view });
385
389
  },
390
+ setNewConversationPrefill(text) {
391
+ set({ newConversationPrefill: text });
392
+ },
386
393
  dismissPreview() {
387
394
  set({ incomingPreview: null });
388
395
  },
@@ -488,6 +495,97 @@ function createWidgetStore() {
488
495
  }));
489
496
  }
490
497
 
498
+ // ../widget-core/src/api.ts
499
+ function createHelpwayAPI(deps) {
500
+ const { store, emitter, identify, apiKey } = deps;
501
+ return {
502
+ open() {
503
+ store.getState().setOpen(true);
504
+ store.getState().setDismissed(false);
505
+ },
506
+ close() {
507
+ store.getState().setOpen(false);
508
+ },
509
+ toggle() {
510
+ const s = store.getState();
511
+ if (s.isDismissed) {
512
+ s.setDismissed(false);
513
+ s.setOpen(true);
514
+ emitter.emit("show");
515
+ } else {
516
+ s.setOpen(!s.isOpen);
517
+ }
518
+ },
519
+ dismiss() {
520
+ store.getState().setOpen(false);
521
+ store.getState().setDismissed(true);
522
+ emitter.emit("dismiss");
523
+ },
524
+ show() {
525
+ store.getState().setDismissed(false);
526
+ emitter.emit("show");
527
+ },
528
+ shutdown() {
529
+ store.getState().setOpen(false);
530
+ store.getState().setDismissed(false);
531
+ clearVisitorId(apiKey);
532
+ if (typeof document !== "undefined") {
533
+ document.cookie = "hw_visitor=; Path=/; Max-Age=0";
534
+ }
535
+ },
536
+ identify,
537
+ newConversation(prefill) {
538
+ const s = store.getState();
539
+ s.setDismissed(false);
540
+ s.setOpen(true);
541
+ s.setNewConversationPrefill(prefill ?? "");
542
+ },
543
+ openConversation(id) {
544
+ const s = store.getState();
545
+ s.setDismissed(false);
546
+ s.setActiveConversation(id);
547
+ s.setOpen(true);
548
+ },
549
+ get unreadCount() {
550
+ return store.getState().conversations.reduce((sum, c) => sum + c.unreadCount, 0);
551
+ },
552
+ get isOpen() {
553
+ return store.getState().isOpen;
554
+ },
555
+ on(event, fn) {
556
+ emitter.on(event, fn);
557
+ },
558
+ off(event, fn) {
559
+ emitter.off(event, fn);
560
+ }
561
+ };
562
+ }
563
+
564
+ // ../widget-core/src/events.ts
565
+ var WidgetEventEmitter = class {
566
+ listeners = /* @__PURE__ */ new Map();
567
+ on(event, fn) {
568
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
569
+ this.listeners.get(event).add(fn);
570
+ }
571
+ off(event, fn) {
572
+ this.listeners.get(event)?.delete(fn);
573
+ }
574
+ emit(event, ...args) {
575
+ const data = args[0];
576
+ this.listeners.get(event)?.forEach((fn) => {
577
+ try {
578
+ fn(data);
579
+ } catch (err) {
580
+ console.error(`[Helpway] Event handler error (${event}):`, err);
581
+ }
582
+ });
583
+ }
584
+ removeAll() {
585
+ this.listeners.clear();
586
+ }
587
+ };
588
+
491
589
  // ../widget-core/src/i18n.ts
492
590
  var en = {
493
591
  // Navigation & tabs
@@ -758,6 +856,10 @@ function SupportProvider({
758
856
  locale,
759
857
  initialConversationId,
760
858
  onReady,
859
+ customLauncherSelector,
860
+ onOpen,
861
+ onClose,
862
+ onUnreadChange,
761
863
  children
762
864
  }) {
763
865
  const initRef = useRef(null);
@@ -771,6 +873,7 @@ function SupportProvider({
771
873
  apiKey
772
874
  });
773
875
  const store2 = createWidgetStore();
876
+ const emitter2 = new WidgetEventEmitter();
774
877
  const ws2 = new WebSocketManager({
775
878
  apiUrl: resolvedApiUrl,
776
879
  visitorId: "",
@@ -794,9 +897,10 @@ function SupportProvider({
794
897
  console.error("[Helpway] identify failed:", err);
795
898
  }
796
899
  };
797
- initRef.current = { client: client2, store: store2, ws: ws2, identify: identify2 };
900
+ const api2 = createHelpwayAPI({ store: store2, emitter: emitter2, identify: identify2, apiKey });
901
+ initRef.current = { client: client2, store: store2, ws: ws2, identify: identify2, api: api2, emitter: emitter2 };
798
902
  }
799
- const { client, store, ws, identify } = initRef.current;
903
+ const { client, store, ws, identify, api, emitter } = initRef.current;
800
904
  const retryBootstrap = useCallback(() => {
801
905
  setHasBootstrapError(false);
802
906
  setBootstrapAttempt((n) => n + 1);
@@ -851,11 +955,8 @@ function SupportProvider({
851
955
  store.getState().setActiveConversation(initialConversationId);
852
956
  store.getState().setOpen(true);
853
957
  }
854
- const openConversation = (id) => {
855
- store.getState().setActiveConversation(id);
856
- store.getState().setOpen(true);
857
- };
858
- onReady?.({ identify: initRef.current.identify, openConversation });
958
+ emitter.emit("ready");
959
+ onReady?.(initRef.current.api);
859
960
  } catch (err) {
860
961
  console.error("[Helpway] Bootstrap failed:", err);
861
962
  if (!cancelled) {
@@ -879,6 +980,13 @@ function SupportProvider({
879
980
  if (event.type === "timelineItemCreated") {
880
981
  const item = event.item;
881
982
  const isPublicIncoming = item?.visibility === "public" && !item.visitorId;
983
+ if (isPublicIncoming && item?.text) {
984
+ emitter.emit("messageReceived", {
985
+ conversationId: event.conversationId,
986
+ text: item.text,
987
+ senderName: "aiAgentId" in item && item.aiAgentId ? "AI" : null
988
+ });
989
+ }
882
990
  if (!isPublicIncoming) return;
883
991
  const s = store.getState();
884
992
  const isViewing = s.isOpen && s.activeConversationId === event.conversationId;
@@ -901,11 +1009,70 @@ function SupportProvider({
901
1009
  setLocale(locale);
902
1010
  }
903
1011
  }, [locale]);
1012
+ useEffect(() => {
1013
+ const unsub = store.subscribe((state, prev) => {
1014
+ if (state.isOpen && !prev.isOpen) {
1015
+ emitter.emit("open");
1016
+ onOpen?.();
1017
+ }
1018
+ if (!state.isOpen && prev.isOpen) {
1019
+ emitter.emit("close");
1020
+ onClose?.();
1021
+ }
1022
+ const prevUnread = prev.conversations.reduce((s, c) => s + c.unreadCount, 0);
1023
+ const nextUnread = state.conversations.reduce((s, c) => s + c.unreadCount, 0);
1024
+ if (nextUnread !== prevUnread) {
1025
+ emitter.emit("unreadChange", nextUnread);
1026
+ onUnreadChange?.(nextUnread);
1027
+ }
1028
+ });
1029
+ return unsub;
1030
+ }, [store, emitter, onOpen, onClose, onUnreadChange]);
1031
+ useEffect(() => {
1032
+ if (!customLauncherSelector) return;
1033
+ const handler = (e) => {
1034
+ e.preventDefault();
1035
+ api.toggle();
1036
+ };
1037
+ const attach = () => {
1038
+ const el2 = document.querySelector(customLauncherSelector);
1039
+ if (el2) {
1040
+ el2.addEventListener("click", handler);
1041
+ return el2;
1042
+ }
1043
+ return null;
1044
+ };
1045
+ let el = attach();
1046
+ let observer = null;
1047
+ if (!el) {
1048
+ observer = new MutationObserver(() => {
1049
+ el = attach();
1050
+ if (el) observer?.disconnect();
1051
+ });
1052
+ observer.observe(document.body, { childList: true, subtree: true });
1053
+ }
1054
+ return () => {
1055
+ el?.removeEventListener("click", handler);
1056
+ observer?.disconnect();
1057
+ };
1058
+ }, [customLauncherSelector, api]);
1059
+ useEffect(() => {
1060
+ if (typeof window === "undefined") return;
1061
+ window.Helpway = api;
1062
+ return () => {
1063
+ if (window.Helpway === api) {
1064
+ window.Helpway = void 0;
1065
+ }
1066
+ emitter.removeAll();
1067
+ };
1068
+ }, [api, emitter]);
904
1069
  const ctxValue = {
905
1070
  client,
906
1071
  store,
907
1072
  ws,
908
1073
  identify,
1074
+ api,
1075
+ emitter,
909
1076
  hasBootstrapError,
910
1077
  wsDisconnected,
911
1078
  retryBootstrap
@@ -3381,12 +3548,13 @@ function AgentPresetIcon({ presetId }) {
3381
3548
  )
3382
3549
  );
3383
3550
  }
3384
- function DefaultWidget({ initialOpen = false } = {}) {
3551
+ function DefaultWidget({ hideLauncher = false } = {}) {
3385
3552
  const { isReady, website, agentConfig } = useWidget();
3386
3553
  useI18n();
3387
3554
  const { client, store, hasBootstrapError, wsDisconnected, retryBootstrap } = useWidgetContext();
3388
3555
  const { conversations, create, isCreating, setActiveConversation } = useConversations();
3389
3556
  const activeId = useWidgetStore((s) => s.activeConversationId);
3557
+ const isDismissed = useWidgetStore((s) => s.isDismissed);
3390
3558
  const { totalUnread } = useUnread();
3391
3559
  const contentTabs = useWidgetStore((s) => s.agentConfig?.contentTabs) ?? EMPTY_CONTENT_TABS;
3392
3560
  const visitorContact = useWidgetStore((s) => s.visitor?.contact);
@@ -3397,13 +3565,30 @@ function DefaultWidget({ initialOpen = false } = {}) {
3397
3565
  activeId
3398
3566
  });
3399
3567
  }
3400
- const [isOpen, setIsOpen] = useState12(initialOpen);
3568
+ const storeIsOpen = useWidgetStore((s) => s.isOpen);
3401
3569
  const [isClosing, setIsClosing] = useState12(false);
3570
+ const isOpen = storeIsOpen || isClosing;
3402
3571
  const [activeTab, setActiveTab] = useState12("home");
3403
3572
  const [subView, setSubView] = useState12(null);
3404
3573
  const [newText, setNewText] = useState12("");
3405
3574
  const [contentDetailSlug, setContentDetailSlug] = useState12(null);
3406
3575
  const [contentCategoryName, setContentCategoryName] = useState12(null);
3576
+ const prevActiveIdRef = useRef13(activeId);
3577
+ useEffect11(() => {
3578
+ if (activeId && activeId !== prevActiveIdRef.current && subView !== "conversation") {
3579
+ setSubView("conversation");
3580
+ }
3581
+ prevActiveIdRef.current = activeId;
3582
+ }, [activeId, subView]);
3583
+ const prefill = useWidgetStore((s) => s.newConversationPrefill);
3584
+ useEffect11(() => {
3585
+ if (prefill !== null) {
3586
+ if (prefill.length > 0) setNewText(prefill);
3587
+ setSubView("new");
3588
+ setActiveTab("home");
3589
+ store.getState().setNewConversationPrefill(null);
3590
+ }
3591
+ }, [prefill, store]);
3407
3592
  const closeTimerRef = useRef13(void 0);
3408
3593
  const welcomeMsgRaw = agentConfig?.welcomeMessage;
3409
3594
  const welcomeIsFactoryDefault = !welcomeMsgRaw || welcomeMsgRaw === DEFAULT_EN_WELCOME;
@@ -3482,9 +3667,9 @@ function DefaultWidget({ initialOpen = false } = {}) {
3482
3667
  },
3483
3668
  [activeTab, subView, activeId, setActiveConversation]
3484
3669
  );
3485
- const storeSetOpen = useWidgetStore((s) => s.setOpen);
3670
+ const setStoreOpen = useWidgetStore((s) => s.setOpen);
3486
3671
  const handleBubbleClick = useCallback12(() => {
3487
- if (isOpen) {
3672
+ if (storeIsOpen) {
3488
3673
  const leavingId = store.getState().activeConversationId;
3489
3674
  if (leavingId && !leavingId.startsWith("temp-")) {
3490
3675
  store.getState().clearUnread(leavingId);
@@ -3493,18 +3678,16 @@ function DefaultWidget({ initialOpen = false } = {}) {
3493
3678
  }
3494
3679
  setIsClosing(true);
3495
3680
  closeTimerRef.current = setTimeout(() => {
3496
- setIsOpen(false);
3497
3681
  setIsClosing(false);
3498
- storeSetOpen(false);
3682
+ setStoreOpen(false);
3499
3683
  }, 200);
3500
3684
  } else {
3501
- setIsOpen(true);
3502
3685
  setIsClosing(false);
3503
3686
  setSubView(null);
3504
3687
  setActiveTab("home");
3505
- storeSetOpen(true);
3688
+ setStoreOpen(true);
3506
3689
  }
3507
- }, [isOpen, storeSetOpen, client, store]);
3690
+ }, [storeIsOpen, setStoreOpen, client, store]);
3508
3691
  const handleClose = useCallback12(() => {
3509
3692
  const leavingId = store.getState().activeConversationId;
3510
3693
  if (leavingId && !leavingId.startsWith("temp-")) {
@@ -3514,11 +3697,10 @@ function DefaultWidget({ initialOpen = false } = {}) {
3514
3697
  }
3515
3698
  setIsClosing(true);
3516
3699
  closeTimerRef.current = setTimeout(() => {
3517
- setIsOpen(false);
3518
3700
  setIsClosing(false);
3519
- storeSetOpen(false);
3701
+ setStoreOpen(false);
3520
3702
  }, 200);
3521
- }, [storeSetOpen, client, store]);
3703
+ }, [setStoreOpen, client, store]);
3522
3704
  const handleNewConversation = useCallback12(async () => {
3523
3705
  const trimmed = newText.trim();
3524
3706
  if (!trimmed || isCreating) return;
@@ -3890,9 +4072,9 @@ function DefaultWidget({ initialOpen = false } = {}) {
3890
4072
  ]
3891
4073
  }
3892
4074
  ),
3893
- !isOpen && /* @__PURE__ */ jsx13(MessagePreview, { onOpen: handleBubbleClick }),
4075
+ !isOpen && !isDismissed && !hideLauncher && /* @__PURE__ */ jsx13(MessagePreview, { onOpen: handleBubbleClick }),
3894
4076
  /* @__PURE__ */ jsx13(WidgetDebugOverlay, {}),
3895
- /* @__PURE__ */ jsxs8(
4077
+ !isDismissed && !hideLauncher && /* @__PURE__ */ jsxs8(
3896
4078
  "button",
3897
4079
  {
3898
4080
  ref: bubbleRef,
@@ -4427,6 +4609,11 @@ function HelpwayWidget({
4427
4609
  locale,
4428
4610
  initialConversationId,
4429
4611
  onReady,
4612
+ hideLauncher,
4613
+ customLauncherSelector,
4614
+ onOpen,
4615
+ onClose,
4616
+ onUnreadChange,
4430
4617
  // eslint-disable-next-line @typescript-eslint/no-deprecated
4431
4618
  src
4432
4619
  }) {
@@ -4439,7 +4626,11 @@ function HelpwayWidget({
4439
4626
  locale,
4440
4627
  initialConversationId,
4441
4628
  onReady,
4442
- children: /* @__PURE__ */ jsx14(DefaultWidget, {})
4629
+ customLauncherSelector,
4630
+ onOpen,
4631
+ onClose,
4632
+ onUnreadChange,
4633
+ children: /* @__PURE__ */ jsx14(DefaultWidget, { hideLauncher })
4443
4634
  }
4444
4635
  );
4445
4636
  }