@cstar.help/js 0.9.0 → 0.10.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/library/index.d.cts +135 -0
- package/dist/library/index.d.ts +135 -0
- package/dist/proactive/index.cjs +481 -0
- package/dist/proactive/index.d.cts +134 -0
- package/dist/proactive/index.d.ts +134 -0
- package/dist/proactive/index.js +456 -0
- package/package.json +11 -1
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/proactive/index.ts
|
|
21
|
+
var proactive_exports = {};
|
|
22
|
+
__export(proactive_exports, {
|
|
23
|
+
ProactiveClient: () => ProactiveClient
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(proactive_exports);
|
|
26
|
+
var DEFAULT_BASE_URL = "https://www.cstar.help";
|
|
27
|
+
function createRageClickDetector(onSignal) {
|
|
28
|
+
let clicks = [];
|
|
29
|
+
let lastFired = 0;
|
|
30
|
+
let handler = null;
|
|
31
|
+
return {
|
|
32
|
+
start() {
|
|
33
|
+
handler = (e) => {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
clicks = clicks.filter((c) => now - c.t < 500);
|
|
36
|
+
clicks.push({ x: e.clientX, y: e.clientY, t: now });
|
|
37
|
+
if (clicks.length >= 3 && now - lastFired > 2e3) {
|
|
38
|
+
const first = clicks[0];
|
|
39
|
+
const allClose = clicks.every((c) => {
|
|
40
|
+
const dx = first.x - c.x;
|
|
41
|
+
const dy = first.y - c.y;
|
|
42
|
+
return Math.sqrt(dx * dx + dy * dy) <= 50;
|
|
43
|
+
});
|
|
44
|
+
if (allClose) {
|
|
45
|
+
lastFired = now;
|
|
46
|
+
const el = e.target;
|
|
47
|
+
let context = el?.tagName ? "on " + el.tagName.toLowerCase() : "";
|
|
48
|
+
if (el?.className && typeof el.className === "string") {
|
|
49
|
+
const cls = el.className.split(" ")[0];
|
|
50
|
+
if (cls) context += "." + cls;
|
|
51
|
+
}
|
|
52
|
+
onSignal({
|
|
53
|
+
type: "rage_click",
|
|
54
|
+
confidence: Math.min(1, clicks.length / 5),
|
|
55
|
+
context: `Clicked ${clicks.length} times rapidly ${context}`,
|
|
56
|
+
timestamp: now
|
|
57
|
+
});
|
|
58
|
+
clicks = [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
document.addEventListener("click", handler, true);
|
|
63
|
+
},
|
|
64
|
+
stop() {
|
|
65
|
+
if (handler) document.removeEventListener("click", handler, true);
|
|
66
|
+
handler = null;
|
|
67
|
+
clicks = [];
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function createExitIntentDetector(onSignal) {
|
|
72
|
+
let fired = false;
|
|
73
|
+
let handler = null;
|
|
74
|
+
return {
|
|
75
|
+
start() {
|
|
76
|
+
if (navigator.maxTouchPoints > 0) return;
|
|
77
|
+
handler = (e) => {
|
|
78
|
+
if (fired || e.clientY > 0 || e.relatedTarget) return;
|
|
79
|
+
fired = true;
|
|
80
|
+
onSignal({
|
|
81
|
+
type: "exit_intent",
|
|
82
|
+
confidence: 0.7,
|
|
83
|
+
context: "Mouse left viewport at top",
|
|
84
|
+
timestamp: Date.now()
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
document.addEventListener("mouseout", handler);
|
|
88
|
+
},
|
|
89
|
+
stop() {
|
|
90
|
+
if (handler) document.removeEventListener("mouseout", handler);
|
|
91
|
+
handler = null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function createIdleDetector(onSignal, timeoutSeconds) {
|
|
96
|
+
let timer = null;
|
|
97
|
+
let fired = false;
|
|
98
|
+
let activityHandler = null;
|
|
99
|
+
const events = ["mousemove", "keydown", "scroll", "touchstart"];
|
|
100
|
+
function resetTimer() {
|
|
101
|
+
fired = false;
|
|
102
|
+
if (timer) clearTimeout(timer);
|
|
103
|
+
timer = setTimeout(() => {
|
|
104
|
+
if (!fired) {
|
|
105
|
+
fired = true;
|
|
106
|
+
onSignal({
|
|
107
|
+
type: "idle",
|
|
108
|
+
confidence: 0.6,
|
|
109
|
+
context: `Idle for ${timeoutSeconds} seconds`,
|
|
110
|
+
timestamp: Date.now()
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}, timeoutSeconds * 1e3);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
start() {
|
|
117
|
+
if (!timeoutSeconds || timeoutSeconds <= 0) return;
|
|
118
|
+
activityHandler = resetTimer;
|
|
119
|
+
events.forEach((evt) => document.addEventListener(evt, activityHandler, { passive: true }));
|
|
120
|
+
resetTimer();
|
|
121
|
+
},
|
|
122
|
+
stop() {
|
|
123
|
+
if (timer) clearTimeout(timer);
|
|
124
|
+
timer = null;
|
|
125
|
+
if (activityHandler) {
|
|
126
|
+
events.forEach((evt) => document.removeEventListener(evt, activityHandler));
|
|
127
|
+
activityHandler = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function createErrorLoopDetector(onSignal) {
|
|
133
|
+
let errors = [];
|
|
134
|
+
let lastFired = 0;
|
|
135
|
+
let errorHandler = null;
|
|
136
|
+
let rejectionHandler = null;
|
|
137
|
+
function recordError(message) {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
errors = errors.filter((e) => now - e.t < 1e4);
|
|
140
|
+
errors.push({ msg: message, t: now });
|
|
141
|
+
if (errors.length >= 3 && now - lastFired > 15e3) {
|
|
142
|
+
lastFired = now;
|
|
143
|
+
const unique = new Set(errors.map((e) => e.msg));
|
|
144
|
+
onSignal({
|
|
145
|
+
type: "error_loop",
|
|
146
|
+
confidence: Math.min(1, errors.length / 5),
|
|
147
|
+
context: `${errors.length} errors in 10s (${unique.size} unique)`,
|
|
148
|
+
timestamp: now
|
|
149
|
+
});
|
|
150
|
+
errors = [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
start() {
|
|
155
|
+
errorHandler = (e) => recordError(e.message || "Unknown error");
|
|
156
|
+
rejectionHandler = (e) => recordError(e.reason?.message || String(e.reason) || "Unhandled rejection");
|
|
157
|
+
window.addEventListener("error", errorHandler);
|
|
158
|
+
window.addEventListener("unhandledrejection", rejectionHandler);
|
|
159
|
+
},
|
|
160
|
+
stop() {
|
|
161
|
+
if (errorHandler) window.removeEventListener("error", errorHandler);
|
|
162
|
+
if (rejectionHandler) window.removeEventListener("unhandledrejection", rejectionHandler);
|
|
163
|
+
errorHandler = null;
|
|
164
|
+
rejectionHandler = null;
|
|
165
|
+
errors = [];
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function createFormAbandonmentDetector(onSignal) {
|
|
170
|
+
const fields = /* @__PURE__ */ new Set();
|
|
171
|
+
let idleTimer = null;
|
|
172
|
+
let fired = false;
|
|
173
|
+
let focusHandler = null;
|
|
174
|
+
let inputHandler = null;
|
|
175
|
+
function isFormField(el) {
|
|
176
|
+
const tag = el.tagName.toLowerCase();
|
|
177
|
+
if (tag !== "input" && tag !== "textarea" && tag !== "select") return false;
|
|
178
|
+
const input = el;
|
|
179
|
+
if (input.type === "search" || el.getAttribute("role") === "searchbox") return false;
|
|
180
|
+
const name = (input.name || "").toLowerCase();
|
|
181
|
+
return name !== "search" && name !== "q" && name !== "query";
|
|
182
|
+
}
|
|
183
|
+
function resetIdleTimer() {
|
|
184
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
185
|
+
if (fields.size >= 2 && !fired) {
|
|
186
|
+
idleTimer = setTimeout(() => {
|
|
187
|
+
fired = true;
|
|
188
|
+
onSignal({
|
|
189
|
+
type: "form_abandonment",
|
|
190
|
+
confidence: 0.65,
|
|
191
|
+
context: `Interacted with ${fields.size} form fields then went idle`,
|
|
192
|
+
timestamp: Date.now()
|
|
193
|
+
});
|
|
194
|
+
}, 3e4);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
start() {
|
|
199
|
+
focusHandler = (e) => {
|
|
200
|
+
if (e.target && isFormField(e.target)) {
|
|
201
|
+
fields.add(e.target);
|
|
202
|
+
resetIdleTimer();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
inputHandler = () => {
|
|
206
|
+
if (fields.size >= 1) resetIdleTimer();
|
|
207
|
+
};
|
|
208
|
+
document.addEventListener("focusin", focusHandler);
|
|
209
|
+
document.addEventListener("input", inputHandler);
|
|
210
|
+
},
|
|
211
|
+
stop() {
|
|
212
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
213
|
+
if (focusHandler) document.removeEventListener("focusin", focusHandler);
|
|
214
|
+
if (inputHandler) document.removeEventListener("input", inputHandler);
|
|
215
|
+
focusHandler = null;
|
|
216
|
+
inputHandler = null;
|
|
217
|
+
fields.clear();
|
|
218
|
+
fired = false;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function createScrollThrashDetector(onSignal) {
|
|
223
|
+
let reversals = [];
|
|
224
|
+
let lastScrollY = -1;
|
|
225
|
+
let lastDirection = 0;
|
|
226
|
+
let lastFired = 0;
|
|
227
|
+
let handler = null;
|
|
228
|
+
return {
|
|
229
|
+
start() {
|
|
230
|
+
handler = () => {
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
const currentY = window.scrollY;
|
|
233
|
+
if (lastScrollY < 0) {
|
|
234
|
+
lastScrollY = currentY;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const diff = currentY - lastScrollY;
|
|
238
|
+
if (Math.abs(diff) < 5) return;
|
|
239
|
+
const direction = diff > 0 ? 1 : -1;
|
|
240
|
+
if (lastDirection !== 0 && direction !== lastDirection) {
|
|
241
|
+
reversals = reversals.filter((t) => now - t < 3e3);
|
|
242
|
+
reversals.push(now);
|
|
243
|
+
if (reversals.length >= 5 && now - lastFired > 5e3) {
|
|
244
|
+
lastFired = now;
|
|
245
|
+
onSignal({
|
|
246
|
+
type: "scroll_thrash",
|
|
247
|
+
confidence: Math.min(1, reversals.length / 8),
|
|
248
|
+
context: `${reversals.length} scroll direction changes in 3s`,
|
|
249
|
+
timestamp: now
|
|
250
|
+
});
|
|
251
|
+
reversals = [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
lastDirection = direction;
|
|
255
|
+
lastScrollY = currentY;
|
|
256
|
+
};
|
|
257
|
+
window.addEventListener("scroll", handler, { passive: true });
|
|
258
|
+
},
|
|
259
|
+
stop() {
|
|
260
|
+
if (handler) window.removeEventListener("scroll", handler);
|
|
261
|
+
handler = null;
|
|
262
|
+
reversals = [];
|
|
263
|
+
lastScrollY = -1;
|
|
264
|
+
lastDirection = 0;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
var TriggerEngine = class {
|
|
269
|
+
constructor(config, onTrigger, isOpen, baseUrl, teamId) {
|
|
270
|
+
this.sessionCount = 0;
|
|
271
|
+
this.lastTriggerTime = 0;
|
|
272
|
+
this.dismissCount = 0;
|
|
273
|
+
this.shownAt = 0;
|
|
274
|
+
this.cooldownSeconds = config.cooldownSeconds;
|
|
275
|
+
this.maxPerSession = config.maxPerSession;
|
|
276
|
+
this.dismissBehavior = config.dismissBehavior;
|
|
277
|
+
this.message = config.message;
|
|
278
|
+
this.onTrigger = onTrigger;
|
|
279
|
+
this.isOpen = isOpen;
|
|
280
|
+
this.baseUrl = baseUrl;
|
|
281
|
+
this.teamId = teamId;
|
|
282
|
+
}
|
|
283
|
+
getEffectiveCooldown() {
|
|
284
|
+
if (this.dismissBehavior === "backoff" && this.dismissCount > 0) {
|
|
285
|
+
return this.cooldownSeconds * Math.pow(2, this.dismissCount);
|
|
286
|
+
}
|
|
287
|
+
return this.cooldownSeconds;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Send analytics beacon (fire-and-forget).
|
|
291
|
+
* Uses text/plain Content-Type to avoid CORS preflight for cross-origin beacons.
|
|
292
|
+
* Falls back to fetch() if sendBeacon is unavailable or fails.
|
|
293
|
+
*/
|
|
294
|
+
sendEvent(eventType, signalType, metadata) {
|
|
295
|
+
const url = this.baseUrl + "/api/widget/proactive-events";
|
|
296
|
+
const payload = JSON.stringify({
|
|
297
|
+
teamId: this.teamId,
|
|
298
|
+
eventType,
|
|
299
|
+
signalType,
|
|
300
|
+
metadata: metadata || {}
|
|
301
|
+
});
|
|
302
|
+
try {
|
|
303
|
+
const queued = navigator.sendBeacon?.(url, new Blob([payload], { type: "text/plain" }));
|
|
304
|
+
if (!queued) throw void 0;
|
|
305
|
+
} catch {
|
|
306
|
+
try {
|
|
307
|
+
fetch(url, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
body: payload,
|
|
310
|
+
headers: { "Content-Type": "text/plain" },
|
|
311
|
+
keepalive: true,
|
|
312
|
+
mode: "cors"
|
|
313
|
+
}).catch(() => {
|
|
314
|
+
});
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
handleSignal(signal) {
|
|
320
|
+
if (this.isOpen()) return;
|
|
321
|
+
if (this.sessionCount >= this.maxPerSession) return;
|
|
322
|
+
if (this.lastTriggerTime > 0) {
|
|
323
|
+
const elapsed = (Date.now() - this.lastTriggerTime) / 1e3;
|
|
324
|
+
if (elapsed < this.getEffectiveCooldown()) return;
|
|
325
|
+
}
|
|
326
|
+
if (this.dismissBehavior === "stop" && this.dismissCount > 0) return;
|
|
327
|
+
this.sessionCount++;
|
|
328
|
+
this.lastTriggerTime = Date.now();
|
|
329
|
+
this.shownAt = Date.now();
|
|
330
|
+
this.sendEvent("trigger_fired", signal.type);
|
|
331
|
+
this.onTrigger({
|
|
332
|
+
message: this.message,
|
|
333
|
+
signalType: signal.type,
|
|
334
|
+
confidence: signal.confidence,
|
|
335
|
+
context: signal.context
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
onEngage(signalType) {
|
|
339
|
+
const timeToEngage = Date.now() - (this.shownAt || Date.now());
|
|
340
|
+
this.sendEvent("engaged", signalType || "unknown", { timeToEngage });
|
|
341
|
+
this.shownAt = 0;
|
|
342
|
+
}
|
|
343
|
+
onDismiss(signalType) {
|
|
344
|
+
this.dismissCount++;
|
|
345
|
+
if (this.dismissBehavior === "stop") {
|
|
346
|
+
this.sessionCount = this.maxPerSession;
|
|
347
|
+
}
|
|
348
|
+
this.sendEvent("dismissed", signalType || "unknown");
|
|
349
|
+
this.shownAt = 0;
|
|
350
|
+
}
|
|
351
|
+
onAutoDismiss(signalType) {
|
|
352
|
+
this.sendEvent("auto_dismissed", signalType || "unknown");
|
|
353
|
+
this.shownAt = 0;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var ProactiveClient = class {
|
|
357
|
+
constructor(config) {
|
|
358
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
359
|
+
this.detectors = [];
|
|
360
|
+
this.triggerEngine = null;
|
|
361
|
+
this.serverConfig = null;
|
|
362
|
+
this._isOpen = false;
|
|
363
|
+
this._destroyed = false;
|
|
364
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
365
|
+
this.teamSlug = config.teamSlug;
|
|
366
|
+
this.init();
|
|
367
|
+
}
|
|
368
|
+
// ============ EVENTS ============
|
|
369
|
+
/**
|
|
370
|
+
* Listen for proactive events.
|
|
371
|
+
*
|
|
372
|
+
* ```typescript
|
|
373
|
+
* proactive.on('trigger', (e) => showNudge(e.message));
|
|
374
|
+
* proactive.on('signal', (e) => console.log('Raw signal:', e.type));
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
on(event, callback) {
|
|
378
|
+
if (!this.listeners.has(event)) {
|
|
379
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
380
|
+
}
|
|
381
|
+
this.listeners.get(event).add(callback);
|
|
382
|
+
return () => {
|
|
383
|
+
this.listeners.get(event)?.delete(callback);
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/** Remove an event listener */
|
|
387
|
+
off(event, callback) {
|
|
388
|
+
this.listeners.get(event)?.delete(callback);
|
|
389
|
+
}
|
|
390
|
+
emit(event, data) {
|
|
391
|
+
this.listeners.get(event)?.forEach((cb) => {
|
|
392
|
+
try {
|
|
393
|
+
cb(data);
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
// ============ USER ACTIONS ============
|
|
399
|
+
/**
|
|
400
|
+
* Call when the user engages with your proactive nudge (clicks CTA, opens chat).
|
|
401
|
+
* Reports analytics and clears internal state.
|
|
402
|
+
*/
|
|
403
|
+
engage(signalType) {
|
|
404
|
+
this.triggerEngine?.onEngage(signalType);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Call when the user explicitly dismisses your nudge (clicks X).
|
|
408
|
+
* Applies backoff or stop behavior based on config.
|
|
409
|
+
*/
|
|
410
|
+
dismiss(signalType) {
|
|
411
|
+
this.triggerEngine?.onDismiss(signalType);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Call when your nudge auto-hides (timeout, not user action).
|
|
415
|
+
* Does NOT count against the session cap.
|
|
416
|
+
*/
|
|
417
|
+
autoDismiss(signalType) {
|
|
418
|
+
this.triggerEngine?.onAutoDismiss(signalType);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Tell the client whether the chat is currently open.
|
|
422
|
+
* Triggers won't fire while the chat is open.
|
|
423
|
+
*/
|
|
424
|
+
setOpen(isOpen) {
|
|
425
|
+
this._isOpen = isOpen;
|
|
426
|
+
}
|
|
427
|
+
/** Stop all detection and clean up */
|
|
428
|
+
destroy() {
|
|
429
|
+
this._destroyed = true;
|
|
430
|
+
this.detectors.forEach((d) => d.stop());
|
|
431
|
+
this.detectors = [];
|
|
432
|
+
this.listeners.clear();
|
|
433
|
+
this.triggerEngine = null;
|
|
434
|
+
}
|
|
435
|
+
// ============ INTERNAL ============
|
|
436
|
+
async init() {
|
|
437
|
+
try {
|
|
438
|
+
const res = await fetch(
|
|
439
|
+
`${this.baseUrl}/widget/api/config?team=${encodeURIComponent(this.teamSlug)}`
|
|
440
|
+
);
|
|
441
|
+
if (!res.ok) throw new Error(`Config fetch failed: ${res.status}`);
|
|
442
|
+
const config = await res.json();
|
|
443
|
+
if (!config.proactive?.enabled) {
|
|
444
|
+
this.emit("error", { message: "Proactive triggers are not enabled for this team" });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (this._destroyed) return;
|
|
448
|
+
this.serverConfig = config.proactive;
|
|
449
|
+
const teamId = config.teamId;
|
|
450
|
+
this.triggerEngine = new TriggerEngine(
|
|
451
|
+
this.serverConfig,
|
|
452
|
+
(event) => this.emit("trigger", event),
|
|
453
|
+
() => this._isOpen,
|
|
454
|
+
this.baseUrl,
|
|
455
|
+
teamId
|
|
456
|
+
);
|
|
457
|
+
const signals = this.serverConfig.signals;
|
|
458
|
+
const onSignal = (signal) => {
|
|
459
|
+
this.emit("signal", signal);
|
|
460
|
+
this.triggerEngine?.handleSignal(signal);
|
|
461
|
+
};
|
|
462
|
+
if (signals.rageClick) this.detectors.push(createRageClickDetector(onSignal));
|
|
463
|
+
if (signals.exitIntent) this.detectors.push(createExitIntentDetector(onSignal));
|
|
464
|
+
if (signals.idleTimeout > 0)
|
|
465
|
+
this.detectors.push(createIdleDetector(onSignal, signals.idleTimeout));
|
|
466
|
+
if (signals.errorLoop) this.detectors.push(createErrorLoopDetector(onSignal));
|
|
467
|
+
if (signals.formAbandonment) this.detectors.push(createFormAbandonmentDetector(onSignal));
|
|
468
|
+
if (signals.scrollThrash) this.detectors.push(createScrollThrashDetector(onSignal));
|
|
469
|
+
this.detectors.forEach((d) => d.start());
|
|
470
|
+
this.emit("ready", void 0);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
this.emit("error", {
|
|
473
|
+
message: err instanceof Error ? err.message : "Failed to initialize proactive detection"
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
479
|
+
0 && (module.exports = {
|
|
480
|
+
ProactiveClient
|
|
481
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proactive Widget Types
|
|
3
|
+
*
|
|
4
|
+
* Types for the ProactiveClient — signal detection and trigger events
|
|
5
|
+
* for SDK users who build their own nudge UI.
|
|
6
|
+
*/
|
|
7
|
+
interface ProactiveConfig {
|
|
8
|
+
/** Team slug (e.g., 'acme') */
|
|
9
|
+
teamSlug: string;
|
|
10
|
+
/** Base URL for the cStar API (default: https://www.cstar.help) */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
interface ProactiveSignals {
|
|
14
|
+
rageClick: boolean;
|
|
15
|
+
exitIntent: boolean;
|
|
16
|
+
idleTimeout: number;
|
|
17
|
+
errorLoop: boolean;
|
|
18
|
+
formAbandonment: boolean;
|
|
19
|
+
scrollThrash: boolean;
|
|
20
|
+
}
|
|
21
|
+
interface ProactiveServerConfig {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
signals: ProactiveSignals;
|
|
24
|
+
message: string;
|
|
25
|
+
cooldownSeconds: number;
|
|
26
|
+
maxPerSession: number;
|
|
27
|
+
displayMode: 'bubble' | 'auto_open';
|
|
28
|
+
dismissBehavior: 'backoff' | 'stop';
|
|
29
|
+
}
|
|
30
|
+
interface ProactiveSignalEvent {
|
|
31
|
+
/** Signal type identifier */
|
|
32
|
+
type: 'rage_click' | 'exit_intent' | 'idle' | 'error_loop' | 'form_abandonment' | 'scroll_thrash';
|
|
33
|
+
/** Confidence score from 0-1 */
|
|
34
|
+
confidence: number;
|
|
35
|
+
/** Human-readable context (e.g., "Clicked 5 times rapidly on .pricing-table") */
|
|
36
|
+
context: string;
|
|
37
|
+
/** When the signal was detected */
|
|
38
|
+
timestamp: number;
|
|
39
|
+
}
|
|
40
|
+
interface ProactiveTriggerEvent {
|
|
41
|
+
/** The configured nudge message (e.g., "Having trouble? We can help!") */
|
|
42
|
+
message: string;
|
|
43
|
+
/** Which signal caused the trigger */
|
|
44
|
+
signalType: string;
|
|
45
|
+
/** Confidence score from the signal */
|
|
46
|
+
confidence: number;
|
|
47
|
+
/** Human-readable signal context */
|
|
48
|
+
context: string;
|
|
49
|
+
}
|
|
50
|
+
type ProactiveEventMap = {
|
|
51
|
+
/** Fires when a trigger passes cooldown/cap checks — show your nudge UI */
|
|
52
|
+
trigger: ProactiveTriggerEvent;
|
|
53
|
+
/** Fires for every raw signal before filtering — for custom trigger logic */
|
|
54
|
+
signal: ProactiveSignalEvent;
|
|
55
|
+
/** Fires when the client connects and starts detecting */
|
|
56
|
+
ready: void;
|
|
57
|
+
/** Fires if config fetch fails or proactive is disabled for this team */
|
|
58
|
+
error: {
|
|
59
|
+
message: string;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
type ProactiveEventType = keyof ProactiveEventMap;
|
|
63
|
+
type ProactiveEventCallback<T extends ProactiveEventType> = (event: ProactiveEventMap[T]) => void;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* ProactiveClient — Signal Detection for Custom UIs
|
|
67
|
+
*
|
|
68
|
+
* Detects frustration signals (rage clicks, exit intent, idle timeout, etc.)
|
|
69
|
+
* on the customer's page and fires events so SDK users can build their own
|
|
70
|
+
* nudge UI. Config is fetched from the team's dashboard settings.
|
|
71
|
+
*
|
|
72
|
+
* ```typescript
|
|
73
|
+
* import { ProactiveClient } from '@cstar.help/js/proactive';
|
|
74
|
+
*
|
|
75
|
+
* const proactive = new ProactiveClient({ teamSlug: 'acme' });
|
|
76
|
+
*
|
|
77
|
+
* proactive.on('trigger', (e) => {
|
|
78
|
+
* showNudge(e.message); // "Having trouble? We can help!"
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* // After showing your nudge, report what happened:
|
|
82
|
+
* proactive.engage(); // user opened chat
|
|
83
|
+
* proactive.dismiss(); // user closed nudge
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
declare class ProactiveClient {
|
|
88
|
+
private readonly baseUrl;
|
|
89
|
+
private readonly teamSlug;
|
|
90
|
+
private listeners;
|
|
91
|
+
private detectors;
|
|
92
|
+
private triggerEngine;
|
|
93
|
+
private serverConfig;
|
|
94
|
+
private _isOpen;
|
|
95
|
+
private _destroyed;
|
|
96
|
+
constructor(config: ProactiveConfig);
|
|
97
|
+
/**
|
|
98
|
+
* Listen for proactive events.
|
|
99
|
+
*
|
|
100
|
+
* ```typescript
|
|
101
|
+
* proactive.on('trigger', (e) => showNudge(e.message));
|
|
102
|
+
* proactive.on('signal', (e) => console.log('Raw signal:', e.type));
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
on<T extends ProactiveEventType>(event: T, callback: ProactiveEventCallback<T>): () => void;
|
|
106
|
+
/** Remove an event listener */
|
|
107
|
+
off<T extends ProactiveEventType>(event: T, callback: ProactiveEventCallback<T>): void;
|
|
108
|
+
private emit;
|
|
109
|
+
/**
|
|
110
|
+
* Call when the user engages with your proactive nudge (clicks CTA, opens chat).
|
|
111
|
+
* Reports analytics and clears internal state.
|
|
112
|
+
*/
|
|
113
|
+
engage(signalType?: string): void;
|
|
114
|
+
/**
|
|
115
|
+
* Call when the user explicitly dismisses your nudge (clicks X).
|
|
116
|
+
* Applies backoff or stop behavior based on config.
|
|
117
|
+
*/
|
|
118
|
+
dismiss(signalType?: string): void;
|
|
119
|
+
/**
|
|
120
|
+
* Call when your nudge auto-hides (timeout, not user action).
|
|
121
|
+
* Does NOT count against the session cap.
|
|
122
|
+
*/
|
|
123
|
+
autoDismiss(signalType?: string): void;
|
|
124
|
+
/**
|
|
125
|
+
* Tell the client whether the chat is currently open.
|
|
126
|
+
* Triggers won't fire while the chat is open.
|
|
127
|
+
*/
|
|
128
|
+
setOpen(isOpen: boolean): void;
|
|
129
|
+
/** Stop all detection and clean up */
|
|
130
|
+
destroy(): void;
|
|
131
|
+
private init;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { ProactiveClient, type ProactiveConfig, type ProactiveEventCallback, type ProactiveEventMap, type ProactiveEventType, type ProactiveServerConfig, type ProactiveSignalEvent, type ProactiveTriggerEvent };
|