@customerhero/js 1.0.1 → 2.0.0
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.cjs +548 -6
- package/dist/index.d.cts +237 -1
- package/dist/index.d.ts +237 -1
- package/dist/index.js +544 -5
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -29,8 +29,11 @@ __export(index_exports, {
|
|
|
29
29
|
captureScreenshot: () => captureScreenshot,
|
|
30
30
|
createTranslator: () => createTranslator,
|
|
31
31
|
detectLocale: () => detectLocale,
|
|
32
|
+
evaluate: () => evaluate,
|
|
32
33
|
isRtlLocale: () => isRtlLocale,
|
|
33
|
-
|
|
34
|
+
pickFire: () => pickFire,
|
|
35
|
+
resolveLocale: () => resolveLocale,
|
|
36
|
+
startTriggersRuntime: () => startTriggersRuntime
|
|
34
37
|
});
|
|
35
38
|
module.exports = __toCommonJS(index_exports);
|
|
36
39
|
|
|
@@ -549,6 +552,377 @@ ${line.slice(5).trim()}` : line.slice(5).trim();
|
|
|
549
552
|
return { event, data };
|
|
550
553
|
}
|
|
551
554
|
|
|
555
|
+
// src/triggers.ts
|
|
556
|
+
function evaluate(node, ctx) {
|
|
557
|
+
if (isGroup(node, "all")) {
|
|
558
|
+
if (node.all.length === 0) return false;
|
|
559
|
+
return node.all.every((child) => evaluate(child, ctx));
|
|
560
|
+
}
|
|
561
|
+
if (isGroup(node, "any")) {
|
|
562
|
+
if (node.any.length === 0) return false;
|
|
563
|
+
return node.any.some((child) => evaluate(child, ctx));
|
|
564
|
+
}
|
|
565
|
+
return evaluateLeaf(node, ctx);
|
|
566
|
+
}
|
|
567
|
+
function isGroup(node, key) {
|
|
568
|
+
return !!node && typeof node === "object" && Array.isArray(node[key]);
|
|
569
|
+
}
|
|
570
|
+
function evaluateLeaf(leaf, ctx) {
|
|
571
|
+
switch (leaf.kind) {
|
|
572
|
+
case "url_path": {
|
|
573
|
+
const path = parseUrlPath(ctx.url);
|
|
574
|
+
switch (leaf.op) {
|
|
575
|
+
case "equals":
|
|
576
|
+
return path === leaf.value;
|
|
577
|
+
case "contains":
|
|
578
|
+
return path.includes(leaf.value);
|
|
579
|
+
case "starts_with":
|
|
580
|
+
return path.startsWith(leaf.value);
|
|
581
|
+
case "regex":
|
|
582
|
+
return safeRegex(leaf.value).test(path);
|
|
583
|
+
default:
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
case "url_query": {
|
|
588
|
+
const has = Object.prototype.hasOwnProperty.call(
|
|
589
|
+
ctx.queryParams,
|
|
590
|
+
leaf.key
|
|
591
|
+
);
|
|
592
|
+
if (leaf.op === "exists") return has;
|
|
593
|
+
if (!has) return false;
|
|
594
|
+
const v = ctx.queryParams[leaf.key] ?? "";
|
|
595
|
+
if (leaf.op === "equals") return v === (leaf.value ?? "");
|
|
596
|
+
if (leaf.op === "contains") return v.includes(leaf.value ?? "");
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
case "referrer": {
|
|
600
|
+
const r = ctx.referrer;
|
|
601
|
+
if (leaf.op === "equals") return r === leaf.value;
|
|
602
|
+
if (leaf.op === "contains") return r.includes(leaf.value);
|
|
603
|
+
if (leaf.op === "regex") return safeRegex(leaf.value).test(r);
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
case "time_on_page":
|
|
607
|
+
return ctx.timeOnPageMs >= leaf.seconds * 1e3;
|
|
608
|
+
case "scroll_depth":
|
|
609
|
+
return ctx.scrollPercent >= leaf.percent;
|
|
610
|
+
case "exit_intent":
|
|
611
|
+
return ctx.exitIntentSeen;
|
|
612
|
+
case "device":
|
|
613
|
+
return ctx.device === leaf.value;
|
|
614
|
+
case "browser_language": {
|
|
615
|
+
const lang = (ctx.language || "").toLowerCase();
|
|
616
|
+
return leaf.values.some((v) => {
|
|
617
|
+
const target = v.toLowerCase();
|
|
618
|
+
return lang === target || lang.startsWith(`${target}-`);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
case "visitor_trait": {
|
|
622
|
+
const has = Object.prototype.hasOwnProperty.call(ctx.traits, leaf.key);
|
|
623
|
+
if (leaf.op === "exists") return has;
|
|
624
|
+
if (!has) return false;
|
|
625
|
+
const v = ctx.traits[leaf.key];
|
|
626
|
+
if (leaf.op === "equals") return v === leaf.value;
|
|
627
|
+
if (leaf.op === "gt")
|
|
628
|
+
return typeof v === "number" && typeof leaf.value === "number" && v > leaf.value;
|
|
629
|
+
if (leaf.op === "lt")
|
|
630
|
+
return typeof v === "number" && typeof leaf.value === "number" && v < leaf.value;
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
case "return_visit": {
|
|
634
|
+
if (leaf.op === "gte") return ctx.returnVisitCount >= leaf.count;
|
|
635
|
+
if (leaf.op === "eq") return ctx.returnVisitCount === leaf.count;
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
default:
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function parseUrlPath(url) {
|
|
643
|
+
try {
|
|
644
|
+
return new URL(url).pathname;
|
|
645
|
+
} catch {
|
|
646
|
+
return url.split("?")[0] ?? url;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function safeRegex(pattern) {
|
|
650
|
+
try {
|
|
651
|
+
return new RegExp(pattern);
|
|
652
|
+
} catch {
|
|
653
|
+
return /^\b$/;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function pickFire(triggers, ctx, firedSet) {
|
|
657
|
+
const sorted = triggers.slice().sort((a, b) => a.priority - b.priority);
|
|
658
|
+
for (const t of sorted) {
|
|
659
|
+
if (t.frequency !== "every_time" && firedSet.has(t.id)) continue;
|
|
660
|
+
if (evaluate(t.conditions, ctx)) return t;
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/triggers-runtime.ts
|
|
666
|
+
var STORAGE_NS = "ch_trigger";
|
|
667
|
+
var RETURN_VISIT_KEY = "ch_visits";
|
|
668
|
+
var SCROLL_THROTTLE_MS = 250;
|
|
669
|
+
var EXIT_INTENT_GRACE_MS = 5e3;
|
|
670
|
+
function getLocalStorage() {
|
|
671
|
+
try {
|
|
672
|
+
return typeof window !== "undefined" ? window.localStorage : null;
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function getSessionStorage() {
|
|
678
|
+
try {
|
|
679
|
+
return typeof window !== "undefined" ? window.sessionStorage : null;
|
|
680
|
+
} catch {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function detectDevice() {
|
|
685
|
+
if (typeof window === "undefined") return "desktop";
|
|
686
|
+
const ua = navigator.userAgent || "";
|
|
687
|
+
if (/iPad|Tablet|PlayBook/i.test(ua)) return "tablet";
|
|
688
|
+
if (/Mobi|Android|iPhone|iPod/i.test(ua)) return "mobile";
|
|
689
|
+
if (window.matchMedia?.("(pointer: coarse)").matches && window.innerWidth < 1024) {
|
|
690
|
+
return window.innerWidth < 600 ? "mobile" : "tablet";
|
|
691
|
+
}
|
|
692
|
+
return "desktop";
|
|
693
|
+
}
|
|
694
|
+
function parseQueryParams(url) {
|
|
695
|
+
const out = {};
|
|
696
|
+
try {
|
|
697
|
+
const u = new URL(url);
|
|
698
|
+
u.searchParams.forEach((v, k) => {
|
|
699
|
+
out[k] = v;
|
|
700
|
+
});
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
function readReturnVisitCount() {
|
|
706
|
+
const ls = getLocalStorage();
|
|
707
|
+
if (!ls) return 1;
|
|
708
|
+
try {
|
|
709
|
+
const raw = ls.getItem(RETURN_VISIT_KEY);
|
|
710
|
+
return raw ? Math.max(1, parseInt(raw, 10) || 1) : 1;
|
|
711
|
+
} catch {
|
|
712
|
+
return 1;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function bumpReturnVisitCount() {
|
|
716
|
+
const ls = getLocalStorage();
|
|
717
|
+
const ss = getSessionStorage();
|
|
718
|
+
if (!ls || !ss) return readReturnVisitCount();
|
|
719
|
+
try {
|
|
720
|
+
if (ss.getItem("ch_visit_seen") === "1") return readReturnVisitCount();
|
|
721
|
+
const current = parseInt(ls.getItem(RETURN_VISIT_KEY) ?? "0", 10) || 0;
|
|
722
|
+
const next = current + 1;
|
|
723
|
+
ls.setItem(RETURN_VISIT_KEY, String(next));
|
|
724
|
+
ss.setItem("ch_visit_seen", "1");
|
|
725
|
+
return next;
|
|
726
|
+
} catch {
|
|
727
|
+
return readReturnVisitCount();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function firedKey(chatbotId, triggerId) {
|
|
731
|
+
return `${STORAGE_NS}_${chatbotId}_${triggerId}_fired`;
|
|
732
|
+
}
|
|
733
|
+
function readFired(chatbotId, triggers) {
|
|
734
|
+
const ls = getLocalStorage();
|
|
735
|
+
const ss = getSessionStorage();
|
|
736
|
+
const out = /* @__PURE__ */ new Set();
|
|
737
|
+
for (const t of triggers) {
|
|
738
|
+
if (t.frequency === "every_time") continue;
|
|
739
|
+
try {
|
|
740
|
+
const key = firedKey(chatbotId, t.id);
|
|
741
|
+
if (t.frequency === "once_ever" && ls?.getItem(key) === "1") {
|
|
742
|
+
out.add(t.id);
|
|
743
|
+
} else if (t.frequency === "once_per_session" && ss?.getItem(key) === "1") {
|
|
744
|
+
out.add(t.id);
|
|
745
|
+
}
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return out;
|
|
750
|
+
}
|
|
751
|
+
function writeFired(chatbotId, triggerId, frequency) {
|
|
752
|
+
if (frequency === "every_time") return;
|
|
753
|
+
const key = firedKey(chatbotId, triggerId);
|
|
754
|
+
try {
|
|
755
|
+
if (frequency === "once_ever") {
|
|
756
|
+
getLocalStorage()?.setItem(key, "1");
|
|
757
|
+
} else if (frequency === "once_per_session") {
|
|
758
|
+
getSessionStorage()?.setItem(key, "1");
|
|
759
|
+
}
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function startTriggersRuntime(options) {
|
|
764
|
+
if (typeof window === "undefined") {
|
|
765
|
+
return {
|
|
766
|
+
stop() {
|
|
767
|
+
},
|
|
768
|
+
reevaluate() {
|
|
769
|
+
},
|
|
770
|
+
setTraits() {
|
|
771
|
+
},
|
|
772
|
+
markFired() {
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
const { chatbotId, triggers, onFire, isAllowedToFire } = options;
|
|
777
|
+
let stopped = false;
|
|
778
|
+
let exitIntentSeen = false;
|
|
779
|
+
let scrollPercent = 0;
|
|
780
|
+
let pageStartedAt = Date.now();
|
|
781
|
+
let timeOnPageMs = 0;
|
|
782
|
+
let lastVisibleAt = Date.now();
|
|
783
|
+
let traits = {
|
|
784
|
+
...options.initialTraits ?? {}
|
|
785
|
+
};
|
|
786
|
+
const firedThisSession = /* @__PURE__ */ new Set();
|
|
787
|
+
for (const id of readFired(chatbotId, triggers)) firedThisSession.add(id);
|
|
788
|
+
const returnVisitCount = bumpReturnVisitCount();
|
|
789
|
+
function snapshot() {
|
|
790
|
+
const url = window.location.href;
|
|
791
|
+
return {
|
|
792
|
+
url,
|
|
793
|
+
queryParams: parseQueryParams(url),
|
|
794
|
+
referrer: document.referrer || "",
|
|
795
|
+
language: (navigator.language || "en").toLowerCase(),
|
|
796
|
+
device: detectDevice(),
|
|
797
|
+
timeOnPageMs,
|
|
798
|
+
scrollPercent,
|
|
799
|
+
exitIntentSeen,
|
|
800
|
+
returnVisitCount,
|
|
801
|
+
traits
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function evaluate2() {
|
|
805
|
+
if (stopped) return;
|
|
806
|
+
if (!isAllowedToFire()) return;
|
|
807
|
+
if (triggers.length === 0) return;
|
|
808
|
+
const ctx = snapshot();
|
|
809
|
+
const trigger = pickFire(triggers, ctx, firedThisSession);
|
|
810
|
+
if (!trigger) return;
|
|
811
|
+
firedThisSession.add(trigger.id);
|
|
812
|
+
writeFired(chatbotId, trigger.id, trigger.frequency);
|
|
813
|
+
try {
|
|
814
|
+
onFire(trigger);
|
|
815
|
+
} catch (err) {
|
|
816
|
+
console.error("CustomerHero: trigger onFire handler threw", err);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const timerId = window.setInterval(() => {
|
|
820
|
+
if (document.hidden) return;
|
|
821
|
+
const now = Date.now();
|
|
822
|
+
timeOnPageMs += now - lastVisibleAt;
|
|
823
|
+
lastVisibleAt = now;
|
|
824
|
+
evaluate2();
|
|
825
|
+
}, 1e3);
|
|
826
|
+
function handleVisibility() {
|
|
827
|
+
const now = Date.now();
|
|
828
|
+
if (document.hidden) {
|
|
829
|
+
timeOnPageMs += now - lastVisibleAt;
|
|
830
|
+
if (now - pageStartedAt > EXIT_INTENT_GRACE_MS && !exitIntentSeen) {
|
|
831
|
+
exitIntentSeen = true;
|
|
832
|
+
evaluate2();
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
lastVisibleAt = now;
|
|
836
|
+
evaluate2();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
840
|
+
let scrollScheduled = false;
|
|
841
|
+
function handleScroll() {
|
|
842
|
+
if (scrollScheduled) return;
|
|
843
|
+
scrollScheduled = true;
|
|
844
|
+
window.setTimeout(() => {
|
|
845
|
+
scrollScheduled = false;
|
|
846
|
+
const doc = document.documentElement;
|
|
847
|
+
const scrollable = doc.scrollHeight - window.innerHeight;
|
|
848
|
+
if (scrollable <= 0) {
|
|
849
|
+
scrollPercent = Math.max(scrollPercent, 100);
|
|
850
|
+
} else {
|
|
851
|
+
const next = window.scrollY / scrollable * 100;
|
|
852
|
+
if (next > scrollPercent) {
|
|
853
|
+
scrollPercent = next;
|
|
854
|
+
evaluate2();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}, SCROLL_THROTTLE_MS);
|
|
858
|
+
}
|
|
859
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
860
|
+
function handleMouseOut(e) {
|
|
861
|
+
if (exitIntentSeen) return;
|
|
862
|
+
if (e.relatedTarget !== null) return;
|
|
863
|
+
if (e.clientY > 0) return;
|
|
864
|
+
if (Date.now() - pageStartedAt < EXIT_INTENT_GRACE_MS) return;
|
|
865
|
+
exitIntentSeen = true;
|
|
866
|
+
evaluate2();
|
|
867
|
+
}
|
|
868
|
+
document.addEventListener("mouseout", handleMouseOut);
|
|
869
|
+
const origPush = history.pushState;
|
|
870
|
+
const origReplace = history.replaceState;
|
|
871
|
+
function onUrlChange() {
|
|
872
|
+
pageStartedAt = Date.now();
|
|
873
|
+
timeOnPageMs = 0;
|
|
874
|
+
lastVisibleAt = Date.now();
|
|
875
|
+
scrollPercent = 0;
|
|
876
|
+
exitIntentSeen = false;
|
|
877
|
+
evaluate2();
|
|
878
|
+
}
|
|
879
|
+
history.pushState = function patched(...args) {
|
|
880
|
+
const ret = origPush.apply(this, args);
|
|
881
|
+
try {
|
|
882
|
+
onUrlChange();
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
return ret;
|
|
886
|
+
};
|
|
887
|
+
history.replaceState = function patched(...args) {
|
|
888
|
+
const ret = origReplace.apply(
|
|
889
|
+
this,
|
|
890
|
+
args
|
|
891
|
+
);
|
|
892
|
+
try {
|
|
893
|
+
onUrlChange();
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
return ret;
|
|
897
|
+
};
|
|
898
|
+
window.addEventListener("popstate", onUrlChange);
|
|
899
|
+
const initialEval = window.setTimeout(evaluate2, 0);
|
|
900
|
+
return {
|
|
901
|
+
stop() {
|
|
902
|
+
stopped = true;
|
|
903
|
+
window.clearInterval(timerId);
|
|
904
|
+
window.clearTimeout(initialEval);
|
|
905
|
+
document.removeEventListener("visibilitychange", handleVisibility);
|
|
906
|
+
window.removeEventListener("scroll", handleScroll);
|
|
907
|
+
document.removeEventListener("mouseout", handleMouseOut);
|
|
908
|
+
window.removeEventListener("popstate", onUrlChange);
|
|
909
|
+
history.pushState = origPush;
|
|
910
|
+
history.replaceState = origReplace;
|
|
911
|
+
},
|
|
912
|
+
reevaluate() {
|
|
913
|
+
evaluate2();
|
|
914
|
+
},
|
|
915
|
+
setTraits(next) {
|
|
916
|
+
traits = { ...traits, ...next };
|
|
917
|
+
evaluate2();
|
|
918
|
+
},
|
|
919
|
+
markFired(triggerId, frequency) {
|
|
920
|
+
firedThisSession.add(triggerId);
|
|
921
|
+
writeFired(chatbotId, triggerId, frequency);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
552
926
|
// src/client.ts
|
|
553
927
|
function resolveConfig(userConfig, fetched) {
|
|
554
928
|
return {
|
|
@@ -603,9 +977,35 @@ var CustomerHeroChat = class {
|
|
|
603
977
|
error: null,
|
|
604
978
|
identity: null,
|
|
605
979
|
locale,
|
|
606
|
-
isRtl: isRtlLocale(locale)
|
|
980
|
+
isRtl: isRtlLocale(locale),
|
|
981
|
+
triggers: [],
|
|
982
|
+
preChatForm: null,
|
|
983
|
+
preChatFormVisible: false,
|
|
984
|
+
preChatSubmission: null,
|
|
985
|
+
consent: this.readStoredConsent(),
|
|
986
|
+
pendingTriggerId: null,
|
|
987
|
+
pendingPrefill: null
|
|
607
988
|
};
|
|
608
989
|
}
|
|
990
|
+
// ── Proactive engagement state ─────────────────────────────────────
|
|
991
|
+
triggersRuntime = null;
|
|
992
|
+
preChatFormSubmitted = false;
|
|
993
|
+
readStoredConsent() {
|
|
994
|
+
try {
|
|
995
|
+
const raw = this.storage?.getItem("ch_consent");
|
|
996
|
+
if (!raw) return { analytics: false };
|
|
997
|
+
const parsed = JSON.parse(raw);
|
|
998
|
+
return { analytics: parsed.analytics === true };
|
|
999
|
+
} catch {
|
|
1000
|
+
return { analytics: false };
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
writeStoredConsent(consent) {
|
|
1004
|
+
try {
|
|
1005
|
+
this.storage?.setItem("ch_consent", JSON.stringify(consent));
|
|
1006
|
+
} catch {
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
609
1009
|
// Switch the active locale at runtime. No-op when the resolved tag matches
|
|
610
1010
|
// the current locale and `stringOverrides` is unchanged. Subscribers get a
|
|
611
1011
|
// single state notification with the new `locale` / `isRtl`.
|
|
@@ -661,8 +1061,16 @@ var CustomerHeroChat = class {
|
|
|
661
1061
|
}
|
|
662
1062
|
const fetched = await response.json();
|
|
663
1063
|
const resolved = resolveConfig(this.userConfig, fetched);
|
|
664
|
-
|
|
1064
|
+
const triggers = Array.isArray(fetched.triggers) ? fetched.triggers : [];
|
|
1065
|
+
const preChatForm = fetched.preChatForm ?? null;
|
|
1066
|
+
this.setState({
|
|
1067
|
+
config: resolved,
|
|
1068
|
+
configLoaded: true,
|
|
1069
|
+
triggers,
|
|
1070
|
+
preChatForm
|
|
1071
|
+
});
|
|
665
1072
|
if (resolved.stringOverrides) this.rebuildTranslator();
|
|
1073
|
+
this.startTriggersRuntimeIfPossible();
|
|
666
1074
|
} catch (error) {
|
|
667
1075
|
const errorMsg = error instanceof Error ? error.message : "Failed to load widget config";
|
|
668
1076
|
console.error("CustomerHero: Failed to fetch widget config", error);
|
|
@@ -720,6 +1128,14 @@ var CustomerHeroChat = class {
|
|
|
720
1128
|
const trimmed = message.trim();
|
|
721
1129
|
const attachmentTokens = options?.attachmentTokens ?? [];
|
|
722
1130
|
if (!trimmed || this.state.isLoading) return;
|
|
1131
|
+
if (this.shouldShowPreChatForm()) {
|
|
1132
|
+
this.pendingMessageAfterPreChat = {
|
|
1133
|
+
message: trimmed,
|
|
1134
|
+
attachmentTokens
|
|
1135
|
+
};
|
|
1136
|
+
this.setState({ preChatFormVisible: true });
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
723
1139
|
const userMsg = {
|
|
724
1140
|
role: "user",
|
|
725
1141
|
content: trimmed,
|
|
@@ -752,7 +1168,13 @@ var CustomerHeroChat = class {
|
|
|
752
1168
|
message: trimmed,
|
|
753
1169
|
...this.state.conversationId ? { conversationId: this.state.conversationId } : {},
|
|
754
1170
|
...this.identityData ? { identity: this.identityData } : {},
|
|
755
|
-
...attachmentTokens.length > 0 ? { attachmentTokens } : {}
|
|
1171
|
+
...attachmentTokens.length > 0 ? { attachmentTokens } : {},
|
|
1172
|
+
// Trigger attribution + pre-chat submission only land on the very
|
|
1173
|
+
// first turn. We only consume them when there's no conversationId
|
|
1174
|
+
// yet — the server ignores them on subsequent turns anyway, but
|
|
1175
|
+
// sending them again would be misleading.
|
|
1176
|
+
...!this.state.conversationId && this.state.pendingTriggerId ? { triggeredByTriggerId: this.state.pendingTriggerId } : {},
|
|
1177
|
+
...!this.state.conversationId && this.state.preChatSubmission ? { prechatSubmission: this.state.preChatSubmission } : {}
|
|
756
1178
|
})
|
|
757
1179
|
});
|
|
758
1180
|
if (!response.ok) {
|
|
@@ -774,7 +1196,11 @@ var CustomerHeroChat = class {
|
|
|
774
1196
|
`ch_conv_${chatbotId}`,
|
|
775
1197
|
meta.conversationId
|
|
776
1198
|
);
|
|
777
|
-
this.setState({
|
|
1199
|
+
this.setState({
|
|
1200
|
+
conversationId: meta.conversationId,
|
|
1201
|
+
pendingTriggerId: null,
|
|
1202
|
+
preChatSubmission: null
|
|
1203
|
+
});
|
|
778
1204
|
}
|
|
779
1205
|
this.patchMessageAt(userMsgIndex, { status: "sent" });
|
|
780
1206
|
if (meta?.messageId) {
|
|
@@ -1047,6 +1473,119 @@ var CustomerHeroChat = class {
|
|
|
1047
1473
|
error: null
|
|
1048
1474
|
});
|
|
1049
1475
|
}
|
|
1476
|
+
// ── Proactive engagement public API ────────────────────────────────
|
|
1477
|
+
/** Update visitor consent. Until `analytics: true` is set, only direct
|
|
1478
|
+
* launcher clicks fire — URL/time/scroll/exit-intent/trait conditions
|
|
1479
|
+
* stay dormant. The setting is persisted in localStorage so revisits
|
|
1480
|
+
* don't re-prompt. */
|
|
1481
|
+
setConsent(consent) {
|
|
1482
|
+
const next = {
|
|
1483
|
+
analytics: typeof consent.analytics === "boolean" ? consent.analytics : this.state.consent.analytics
|
|
1484
|
+
};
|
|
1485
|
+
this.writeStoredConsent(next);
|
|
1486
|
+
this.setState({ consent: next });
|
|
1487
|
+
this.triggersRuntime?.reevaluate();
|
|
1488
|
+
}
|
|
1489
|
+
/** Set or update visitor traits used by trait-based conditions. The trait
|
|
1490
|
+
* values are kept in memory (not persisted) so the integrator decides
|
|
1491
|
+
* the source of truth. */
|
|
1492
|
+
setTraits(traits) {
|
|
1493
|
+
this.triggersRuntime?.setTraits(traits);
|
|
1494
|
+
}
|
|
1495
|
+
/** Submit pre-chat form answers. Synthesizes a customer record server-side
|
|
1496
|
+
* on the next sendMessage. Resumes any pending message that was deferred
|
|
1497
|
+
* while the form was open. */
|
|
1498
|
+
async submitPreChatForm(submission) {
|
|
1499
|
+
this.preChatFormSubmitted = true;
|
|
1500
|
+
this.setState({
|
|
1501
|
+
preChatSubmission: submission,
|
|
1502
|
+
preChatFormVisible: false
|
|
1503
|
+
});
|
|
1504
|
+
const pending = this.pendingMessageAfterPreChat;
|
|
1505
|
+
this.pendingMessageAfterPreChat = null;
|
|
1506
|
+
if (pending) {
|
|
1507
|
+
await this.sendMessage(pending.message, {
|
|
1508
|
+
attachmentTokens: pending.attachmentTokens
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
/** Dismiss the pre-chat form without submitting. The form will reappear
|
|
1513
|
+
* on the next sendMessage attempt — call `setConsent` to acknowledge a
|
|
1514
|
+
* refusal, or `reset()` to clear pending state. */
|
|
1515
|
+
cancelPreChatForm() {
|
|
1516
|
+
this.pendingMessageAfterPreChat = null;
|
|
1517
|
+
this.setState({ preChatFormVisible: false });
|
|
1518
|
+
}
|
|
1519
|
+
/** Programmatically dispatch the action attached to a trigger. Used by
|
|
1520
|
+
* integrators who want to act on a custom button, e.g. an exit-intent
|
|
1521
|
+
* modal in their own UI. */
|
|
1522
|
+
fireTrigger(triggerId) {
|
|
1523
|
+
const trigger = this.state.triggers.find((t) => t.id === triggerId);
|
|
1524
|
+
if (!trigger) return;
|
|
1525
|
+
this.triggersRuntime?.markFired(trigger.id, trigger.frequency);
|
|
1526
|
+
this.handleTriggerAction(trigger);
|
|
1527
|
+
}
|
|
1528
|
+
// ── Internals ──────────────────────────────────────────────────────
|
|
1529
|
+
pendingMessageAfterPreChat = null;
|
|
1530
|
+
shouldShowPreChatForm() {
|
|
1531
|
+
const form = this.state.preChatForm;
|
|
1532
|
+
if (!form) return false;
|
|
1533
|
+
if (this.preChatFormSubmitted) return false;
|
|
1534
|
+
if (this.state.conversationId) return false;
|
|
1535
|
+
if (form.skipForIdentified && this.identityData?.userId) return false;
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
startTriggersRuntimeIfPossible() {
|
|
1539
|
+
if (this.triggersRuntime) return;
|
|
1540
|
+
if (this.state.triggers.length === 0) return;
|
|
1541
|
+
this.triggersRuntime = startTriggersRuntime({
|
|
1542
|
+
chatbotId: this.state.config.chatbotId,
|
|
1543
|
+
triggers: this.state.triggers,
|
|
1544
|
+
isAllowedToFire: () => this.state.consent.analytics,
|
|
1545
|
+
onFire: (trigger) => this.handleTriggerAction(trigger)
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
handleTriggerAction(trigger) {
|
|
1549
|
+
this.setState({ pendingTriggerId: trigger.id });
|
|
1550
|
+
const action = trigger.action;
|
|
1551
|
+
switch (action.kind) {
|
|
1552
|
+
case "open_widget":
|
|
1553
|
+
this.open();
|
|
1554
|
+
break;
|
|
1555
|
+
case "open_with_prefill":
|
|
1556
|
+
this.open();
|
|
1557
|
+
this.setState({ pendingPrefill: action.prefill });
|
|
1558
|
+
break;
|
|
1559
|
+
case "show_form":
|
|
1560
|
+
this.open();
|
|
1561
|
+
if (this.state.preChatForm) {
|
|
1562
|
+
this.setState({ preChatFormVisible: true });
|
|
1563
|
+
}
|
|
1564
|
+
break;
|
|
1565
|
+
case "send_message":
|
|
1566
|
+
this.open();
|
|
1567
|
+
if (this.state.messages.length === 0) {
|
|
1568
|
+
this.setState({
|
|
1569
|
+
messages: [{ role: "bot", content: action.message }]
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/** Read and clear the pending prefill (set by an `open_with_prefill`
|
|
1576
|
+
* trigger). The host calls this once when mounting the input and seeds
|
|
1577
|
+
* its controlled value with the result. */
|
|
1578
|
+
consumePendingPrefill() {
|
|
1579
|
+
const prefill = this.state.pendingPrefill;
|
|
1580
|
+
if (prefill !== null) this.setState({ pendingPrefill: null });
|
|
1581
|
+
return prefill;
|
|
1582
|
+
}
|
|
1583
|
+
/** Stop the triggers runtime and detach listeners. Safe to call multiple
|
|
1584
|
+
* times; safe to call before the runtime started. */
|
|
1585
|
+
destroy() {
|
|
1586
|
+
this.triggersRuntime?.stop();
|
|
1587
|
+
this.triggersRuntime = null;
|
|
1588
|
+
}
|
|
1050
1589
|
identify(payload) {
|
|
1051
1590
|
const { userId, email, name, phone, company, userHash, ...rest } = payload;
|
|
1052
1591
|
const customProperties = {};
|
|
@@ -1253,6 +1792,9 @@ async function canvasToBlob(canvas, quality) {
|
|
|
1253
1792
|
captureScreenshot,
|
|
1254
1793
|
createTranslator,
|
|
1255
1794
|
detectLocale,
|
|
1795
|
+
evaluate,
|
|
1256
1796
|
isRtlLocale,
|
|
1257
|
-
|
|
1797
|
+
pickFire,
|
|
1798
|
+
resolveLocale,
|
|
1799
|
+
startTriggersRuntime
|
|
1258
1800
|
});
|