@prodact.ai/sdk 0.0.2 → 0.0.5
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/cdn/sdk.global.js +367 -0
- package/dist/cdn/sdk.global.js.map +1 -0
- package/dist/index.js +2153 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2136 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.js +2906 -409
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +2921 -425
- package/dist/react.mjs.map +1 -1
- package/package.json +11 -2
- package/dist/index.d.mts +0 -135
- package/dist/index.d.ts +0 -135
- package/dist/react.d.mts +0 -244
- package/dist/react.d.ts +0 -244
package/dist/react.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(react_exports, {
|
|
|
35
35
|
ProduckContext: () => ProduckContext,
|
|
36
36
|
ProduckProvider: () => ProduckProvider,
|
|
37
37
|
ProduckTarget: () => ProduckTarget,
|
|
38
|
+
VisualFlowDisplay: () => VisualFlowDisplay,
|
|
38
39
|
useProduck: () => useProduck,
|
|
39
40
|
useProduckAction: () => useProduckAction,
|
|
40
41
|
useProduckFlow: () => useProduckFlow,
|
|
@@ -46,6 +47,1689 @@ module.exports = __toCommonJS(react_exports);
|
|
|
46
47
|
// src/react/ProduckProvider.tsx
|
|
47
48
|
var import_react2 = require("react");
|
|
48
49
|
|
|
50
|
+
// src/recording/utils.ts
|
|
51
|
+
function generateSessionId() {
|
|
52
|
+
const timestamp = Date.now().toString(36);
|
|
53
|
+
const randomPart = Math.random().toString(36).substring(2, 15);
|
|
54
|
+
return `rec_${timestamp}_${randomPart}`;
|
|
55
|
+
}
|
|
56
|
+
function getDeviceInfo() {
|
|
57
|
+
if (typeof window === "undefined") {
|
|
58
|
+
return {
|
|
59
|
+
userAgent: "unknown",
|
|
60
|
+
screenWidth: 0,
|
|
61
|
+
screenHeight: 0,
|
|
62
|
+
viewportWidth: 0,
|
|
63
|
+
viewportHeight: 0,
|
|
64
|
+
devicePixelRatio: 1,
|
|
65
|
+
language: "en",
|
|
66
|
+
platform: "unknown"
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
userAgent: navigator.userAgent,
|
|
71
|
+
screenWidth: screen.width,
|
|
72
|
+
screenHeight: screen.height,
|
|
73
|
+
viewportWidth: window.innerWidth,
|
|
74
|
+
viewportHeight: window.innerHeight,
|
|
75
|
+
devicePixelRatio: window.devicePixelRatio || 1,
|
|
76
|
+
language: navigator.language,
|
|
77
|
+
platform: navigator.platform
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var DEFAULT_PRIVACY_CONFIG = {
|
|
81
|
+
maskAllInputs: true,
|
|
82
|
+
maskAllText: false,
|
|
83
|
+
maskSelectors: [".sensitive", "[data-sensitive]", ".pii", "[data-pii]"],
|
|
84
|
+
blockSelectors: [".no-record", "[data-no-record]"],
|
|
85
|
+
maskInputTypes: ["password", "email", "tel", "credit-card"],
|
|
86
|
+
maskCharacter: "*"
|
|
87
|
+
};
|
|
88
|
+
var DEFAULT_PERFORMANCE_CONFIG = {
|
|
89
|
+
batchEvents: true,
|
|
90
|
+
batchSize: 50,
|
|
91
|
+
batchTimeout: 5e3,
|
|
92
|
+
compress: true,
|
|
93
|
+
mouseMoveThrottle: 50,
|
|
94
|
+
maxDuration: 30 * 60 * 1e3
|
|
95
|
+
// 30 minutes
|
|
96
|
+
};
|
|
97
|
+
var DEFAULT_CAPTURE_CONFIG = {
|
|
98
|
+
console: true,
|
|
99
|
+
network: false,
|
|
100
|
+
performance: false,
|
|
101
|
+
errors: true,
|
|
102
|
+
customEvents: true
|
|
103
|
+
};
|
|
104
|
+
function mergeConfig(userConfig) {
|
|
105
|
+
return {
|
|
106
|
+
enabled: userConfig?.enabled ?? false,
|
|
107
|
+
samplingRate: userConfig?.samplingRate ?? 1,
|
|
108
|
+
privacy: { ...DEFAULT_PRIVACY_CONFIG, ...userConfig?.privacy },
|
|
109
|
+
performance: { ...DEFAULT_PERFORMANCE_CONFIG, ...userConfig?.performance },
|
|
110
|
+
capture: { ...DEFAULT_CAPTURE_CONFIG, ...userConfig?.capture },
|
|
111
|
+
endpoint: userConfig?.endpoint ?? "",
|
|
112
|
+
onStart: userConfig?.onStart ?? (() => {
|
|
113
|
+
}),
|
|
114
|
+
onStop: userConfig?.onStop ?? (() => {
|
|
115
|
+
}),
|
|
116
|
+
onError: userConfig?.onError ?? (() => {
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function shouldRecordSession(samplingRate) {
|
|
121
|
+
if (samplingRate >= 1) return true;
|
|
122
|
+
if (samplingRate <= 0) return false;
|
|
123
|
+
return Math.random() < samplingRate;
|
|
124
|
+
}
|
|
125
|
+
function compressEvents(events) {
|
|
126
|
+
try {
|
|
127
|
+
const json = JSON.stringify(events);
|
|
128
|
+
if (typeof btoa !== "undefined") {
|
|
129
|
+
return btoa(encodeURIComponent(json));
|
|
130
|
+
}
|
|
131
|
+
return json;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.warn("[Recording] Failed to compress events:", error);
|
|
134
|
+
return JSON.stringify(events);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function isBrowser() {
|
|
138
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
139
|
+
}
|
|
140
|
+
function getCurrentUrl() {
|
|
141
|
+
if (!isBrowser()) return "";
|
|
142
|
+
return window.location.href;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/recording/recorder.ts
|
|
146
|
+
var SessionRecorder = class {
|
|
147
|
+
constructor(config) {
|
|
148
|
+
this.state = "idle";
|
|
149
|
+
this.sessionId = null;
|
|
150
|
+
this.sdkSessionToken = null;
|
|
151
|
+
this.apiUrl = "";
|
|
152
|
+
this.sdkKey = null;
|
|
153
|
+
// Recording internals
|
|
154
|
+
this.stopRecording = null;
|
|
155
|
+
this.events = [];
|
|
156
|
+
this.eventCount = 0;
|
|
157
|
+
this.batchIndex = 0;
|
|
158
|
+
this.batchesSent = 0;
|
|
159
|
+
this.errorCount = 0;
|
|
160
|
+
this.startTime = null;
|
|
161
|
+
this.batchTimeout = null;
|
|
162
|
+
this.maxDurationTimeout = null;
|
|
163
|
+
// Cleanup handlers
|
|
164
|
+
this.consoleCleanup = null;
|
|
165
|
+
this.errorCleanup = null;
|
|
166
|
+
this.config = mergeConfig(config);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Initialize the recorder with SDK context
|
|
170
|
+
*/
|
|
171
|
+
initialize(sdkSessionToken, apiUrl, sdkKey) {
|
|
172
|
+
this.sdkSessionToken = sdkSessionToken;
|
|
173
|
+
this.apiUrl = apiUrl;
|
|
174
|
+
this.sdkKey = sdkKey || null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Update configuration
|
|
178
|
+
*/
|
|
179
|
+
updateConfig(config) {
|
|
180
|
+
this.config = mergeConfig({ ...this.config, ...config });
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Start recording session
|
|
184
|
+
*/
|
|
185
|
+
async start() {
|
|
186
|
+
if (!isBrowser()) {
|
|
187
|
+
console.warn("[Recording] Cannot record outside browser environment");
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (!this.config.enabled) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (this.state === "recording") {
|
|
194
|
+
console.warn("[Recording] Already recording");
|
|
195
|
+
return this.sessionId;
|
|
196
|
+
}
|
|
197
|
+
if (!shouldRecordSession(this.config.samplingRate)) {
|
|
198
|
+
console.log("[Recording] Session not sampled for recording");
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const rrweb = await import("rrweb");
|
|
203
|
+
this.sessionId = generateSessionId();
|
|
204
|
+
this.events = [];
|
|
205
|
+
this.eventCount = 0;
|
|
206
|
+
this.batchIndex = 0;
|
|
207
|
+
this.batchesSent = 0;
|
|
208
|
+
this.errorCount = 0;
|
|
209
|
+
this.startTime = /* @__PURE__ */ new Date();
|
|
210
|
+
this.state = "recording";
|
|
211
|
+
const rrwebOptions = {
|
|
212
|
+
emit: (event) => this.handleEvent(event),
|
|
213
|
+
maskAllInputs: this.config.privacy.maskAllInputs,
|
|
214
|
+
maskTextSelector: this.config.privacy.maskSelectors?.join(",") || void 0,
|
|
215
|
+
blockSelector: this.config.privacy.blockSelectors?.join(",") || void 0,
|
|
216
|
+
maskInputOptions: {
|
|
217
|
+
password: true,
|
|
218
|
+
email: this.config.privacy.maskInputTypes?.includes("email"),
|
|
219
|
+
tel: this.config.privacy.maskInputTypes?.includes("tel")
|
|
220
|
+
},
|
|
221
|
+
sampling: {
|
|
222
|
+
mousemove: true,
|
|
223
|
+
mouseInteraction: true,
|
|
224
|
+
scroll: 150,
|
|
225
|
+
media: 800,
|
|
226
|
+
input: "last"
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
this.stopRecording = rrweb.record(rrwebOptions);
|
|
230
|
+
if (this.config.capture.console) {
|
|
231
|
+
this.setupConsoleCapture();
|
|
232
|
+
}
|
|
233
|
+
if (this.config.capture.errors) {
|
|
234
|
+
this.setupErrorCapture();
|
|
235
|
+
}
|
|
236
|
+
if (this.config.performance.maxDuration) {
|
|
237
|
+
this.maxDurationTimeout = setTimeout(() => {
|
|
238
|
+
console.log("[Recording] Max duration reached, stopping");
|
|
239
|
+
this.stop();
|
|
240
|
+
}, this.config.performance.maxDuration);
|
|
241
|
+
}
|
|
242
|
+
await this.sendSessionStart();
|
|
243
|
+
this.config.onStart(this.sessionId);
|
|
244
|
+
console.log("[Recording] Started session:", this.sessionId);
|
|
245
|
+
return this.sessionId;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error("[Recording] Failed to start:", error);
|
|
248
|
+
this.state = "idle";
|
|
249
|
+
this.config.onError(error instanceof Error ? error : new Error(String(error)));
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Stop recording
|
|
255
|
+
*/
|
|
256
|
+
async stop() {
|
|
257
|
+
if (this.state !== "recording" && this.state !== "paused") {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
if (this.stopRecording) {
|
|
262
|
+
this.stopRecording();
|
|
263
|
+
this.stopRecording = null;
|
|
264
|
+
}
|
|
265
|
+
if (this.consoleCleanup) {
|
|
266
|
+
this.consoleCleanup();
|
|
267
|
+
this.consoleCleanup = null;
|
|
268
|
+
}
|
|
269
|
+
if (this.errorCleanup) {
|
|
270
|
+
this.errorCleanup();
|
|
271
|
+
this.errorCleanup = null;
|
|
272
|
+
}
|
|
273
|
+
if (this.batchTimeout) {
|
|
274
|
+
clearTimeout(this.batchTimeout);
|
|
275
|
+
this.batchTimeout = null;
|
|
276
|
+
}
|
|
277
|
+
if (this.maxDurationTimeout) {
|
|
278
|
+
clearTimeout(this.maxDurationTimeout);
|
|
279
|
+
this.maxDurationTimeout = null;
|
|
280
|
+
}
|
|
281
|
+
if (this.events.length > 0) {
|
|
282
|
+
await this.sendBatch(true);
|
|
283
|
+
}
|
|
284
|
+
await this.sendSessionEnd();
|
|
285
|
+
const sessionId = this.sessionId;
|
|
286
|
+
const eventCount = this.eventCount;
|
|
287
|
+
this.state = "stopped";
|
|
288
|
+
this.config.onStop(sessionId || "", eventCount);
|
|
289
|
+
console.log("[Recording] Stopped session:", sessionId, "Events:", eventCount);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error("[Recording] Error stopping:", error);
|
|
292
|
+
this.config.onError(error instanceof Error ? error : new Error(String(error)));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Pause recording
|
|
297
|
+
*/
|
|
298
|
+
pause() {
|
|
299
|
+
if (this.state !== "recording") return;
|
|
300
|
+
this.state = "paused";
|
|
301
|
+
console.log("[Recording] Paused");
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Resume recording
|
|
305
|
+
*/
|
|
306
|
+
resume() {
|
|
307
|
+
if (this.state !== "paused") return;
|
|
308
|
+
this.state = "recording";
|
|
309
|
+
console.log("[Recording] Resumed");
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Add a custom event to the recording
|
|
313
|
+
*/
|
|
314
|
+
addCustomEvent(event) {
|
|
315
|
+
if (this.state !== "recording" || !this.config.capture.customEvents) return;
|
|
316
|
+
const customEvent = {
|
|
317
|
+
type: 5,
|
|
318
|
+
// rrweb custom event type
|
|
319
|
+
data: {
|
|
320
|
+
tag: "custom",
|
|
321
|
+
payload: event
|
|
322
|
+
},
|
|
323
|
+
timestamp: event.timestamp || Date.now()
|
|
324
|
+
};
|
|
325
|
+
this.handleEvent(customEvent);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get current recording stats
|
|
329
|
+
*/
|
|
330
|
+
getStats() {
|
|
331
|
+
const duration = this.startTime ? Date.now() - this.startTime.getTime() : 0;
|
|
332
|
+
return {
|
|
333
|
+
state: this.state,
|
|
334
|
+
sessionId: this.sessionId,
|
|
335
|
+
eventCount: this.eventCount,
|
|
336
|
+
startTime: this.startTime,
|
|
337
|
+
duration,
|
|
338
|
+
batchesSent: this.batchesSent,
|
|
339
|
+
errors: this.errorCount
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Check if recording is active
|
|
344
|
+
*/
|
|
345
|
+
isRecording() {
|
|
346
|
+
return this.state === "recording";
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get session ID
|
|
350
|
+
*/
|
|
351
|
+
getSessionId() {
|
|
352
|
+
return this.sessionId;
|
|
353
|
+
}
|
|
354
|
+
// ============ Private Methods ============
|
|
355
|
+
/**
|
|
356
|
+
* Handle incoming event from rrweb
|
|
357
|
+
*/
|
|
358
|
+
handleEvent(event) {
|
|
359
|
+
if (this.state !== "recording") return;
|
|
360
|
+
this.events.push(event);
|
|
361
|
+
this.eventCount++;
|
|
362
|
+
if (this.config.performance.batchEvents) {
|
|
363
|
+
if (this.events.length >= this.config.performance.batchSize) {
|
|
364
|
+
this.sendBatch();
|
|
365
|
+
} else if (!this.batchTimeout) {
|
|
366
|
+
this.batchTimeout = setTimeout(() => {
|
|
367
|
+
this.batchTimeout = null;
|
|
368
|
+
if (this.events.length > 0) {
|
|
369
|
+
this.sendBatch();
|
|
370
|
+
}
|
|
371
|
+
}, this.config.performance.batchTimeout);
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
this.sendBatch();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Send event batch to backend
|
|
379
|
+
*/
|
|
380
|
+
async sendBatch(isFinal = false) {
|
|
381
|
+
if (this.events.length === 0) return;
|
|
382
|
+
const eventsToSend = [...this.events];
|
|
383
|
+
this.events = [];
|
|
384
|
+
const batch = {
|
|
385
|
+
sessionId: this.sessionId,
|
|
386
|
+
sdkSessionToken: this.sdkSessionToken,
|
|
387
|
+
events: eventsToSend,
|
|
388
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
389
|
+
batchIndex: this.batchIndex++,
|
|
390
|
+
isFinal
|
|
391
|
+
};
|
|
392
|
+
try {
|
|
393
|
+
const endpoint = this.config.endpoint || `${this.apiUrl}/session-recordings/events`;
|
|
394
|
+
const headers = { "Content-Type": "application/json" };
|
|
395
|
+
if (this.sdkKey) {
|
|
396
|
+
headers["X-SDK-Key"] = this.sdkKey;
|
|
397
|
+
}
|
|
398
|
+
const body = this.config.performance.compress ? JSON.stringify({ ...batch, events: void 0, compressedEvents: compressEvents(eventsToSend) }) : JSON.stringify(batch);
|
|
399
|
+
await fetch(endpoint, {
|
|
400
|
+
method: "POST",
|
|
401
|
+
headers,
|
|
402
|
+
body
|
|
403
|
+
});
|
|
404
|
+
this.batchesSent++;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error("[Recording] Failed to send batch:", error);
|
|
407
|
+
this.errorCount++;
|
|
408
|
+
this.events = [...eventsToSend, ...this.events];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Send session start notification
|
|
413
|
+
*/
|
|
414
|
+
async sendSessionStart() {
|
|
415
|
+
const session = {
|
|
416
|
+
id: this.sessionId,
|
|
417
|
+
sdkSessionToken: this.sdkSessionToken,
|
|
418
|
+
startUrl: getCurrentUrl(),
|
|
419
|
+
startTime: this.startTime,
|
|
420
|
+
deviceInfo: getDeviceInfo()
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
const endpoint = `${this.apiUrl}/session-recordings/start`;
|
|
424
|
+
const headers = { "Content-Type": "application/json" };
|
|
425
|
+
if (this.sdkKey) {
|
|
426
|
+
headers["X-SDK-Key"] = this.sdkKey;
|
|
427
|
+
}
|
|
428
|
+
await fetch(endpoint, {
|
|
429
|
+
method: "POST",
|
|
430
|
+
headers,
|
|
431
|
+
body: JSON.stringify(session)
|
|
432
|
+
});
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error("[Recording] Failed to send session start:", error);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Send session end notification
|
|
439
|
+
*/
|
|
440
|
+
async sendSessionEnd() {
|
|
441
|
+
try {
|
|
442
|
+
const endpoint = `${this.apiUrl}/session-recordings/end`;
|
|
443
|
+
const headers = { "Content-Type": "application/json" };
|
|
444
|
+
if (this.sdkKey) {
|
|
445
|
+
headers["X-SDK-Key"] = this.sdkKey;
|
|
446
|
+
}
|
|
447
|
+
await fetch(endpoint, {
|
|
448
|
+
method: "POST",
|
|
449
|
+
headers,
|
|
450
|
+
body: JSON.stringify({
|
|
451
|
+
sessionId: this.sessionId,
|
|
452
|
+
endTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
453
|
+
eventCount: this.eventCount
|
|
454
|
+
})
|
|
455
|
+
});
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.error("[Recording] Failed to send session end:", error);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Setup console log capture
|
|
462
|
+
*/
|
|
463
|
+
setupConsoleCapture() {
|
|
464
|
+
const originalConsole = {
|
|
465
|
+
log: console.log,
|
|
466
|
+
info: console.info,
|
|
467
|
+
warn: console.warn,
|
|
468
|
+
error: console.error,
|
|
469
|
+
debug: console.debug
|
|
470
|
+
};
|
|
471
|
+
const captureConsole = (level) => {
|
|
472
|
+
return (...args) => {
|
|
473
|
+
originalConsole[level](...args);
|
|
474
|
+
if (args[0]?.toString().includes("[Recording]")) return;
|
|
475
|
+
const logEvent = {
|
|
476
|
+
level,
|
|
477
|
+
message: args.map(
|
|
478
|
+
(arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
|
|
479
|
+
).join(" "),
|
|
480
|
+
timestamp: Date.now()
|
|
481
|
+
};
|
|
482
|
+
this.addCustomEvent({
|
|
483
|
+
type: "console",
|
|
484
|
+
data: logEvent
|
|
485
|
+
});
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
console.log = captureConsole("log");
|
|
489
|
+
console.info = captureConsole("info");
|
|
490
|
+
console.warn = captureConsole("warn");
|
|
491
|
+
console.error = captureConsole("error");
|
|
492
|
+
console.debug = captureConsole("debug");
|
|
493
|
+
this.consoleCleanup = () => {
|
|
494
|
+
console.log = originalConsole.log;
|
|
495
|
+
console.info = originalConsole.info;
|
|
496
|
+
console.warn = originalConsole.warn;
|
|
497
|
+
console.error = originalConsole.error;
|
|
498
|
+
console.debug = originalConsole.debug;
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Setup error capture
|
|
503
|
+
*/
|
|
504
|
+
setupErrorCapture() {
|
|
505
|
+
const errorHandler = (event) => {
|
|
506
|
+
const errorEvent = {
|
|
507
|
+
type: "error",
|
|
508
|
+
message: event.message,
|
|
509
|
+
filename: event.filename,
|
|
510
|
+
lineno: event.lineno,
|
|
511
|
+
colno: event.colno,
|
|
512
|
+
timestamp: Date.now()
|
|
513
|
+
};
|
|
514
|
+
this.addCustomEvent({
|
|
515
|
+
type: "error",
|
|
516
|
+
data: errorEvent
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
const rejectionHandler = (event) => {
|
|
520
|
+
const errorEvent = {
|
|
521
|
+
type: "unhandledrejection",
|
|
522
|
+
message: event.reason?.message || String(event.reason),
|
|
523
|
+
stack: event.reason?.stack,
|
|
524
|
+
timestamp: Date.now()
|
|
525
|
+
};
|
|
526
|
+
this.addCustomEvent({
|
|
527
|
+
type: "error",
|
|
528
|
+
data: errorEvent
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
window.addEventListener("error", errorHandler);
|
|
532
|
+
window.addEventListener("unhandledrejection", rejectionHandler);
|
|
533
|
+
this.errorCleanup = () => {
|
|
534
|
+
window.removeEventListener("error", errorHandler);
|
|
535
|
+
window.removeEventListener("unhandledrejection", rejectionHandler);
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Destroy the recorder
|
|
540
|
+
*/
|
|
541
|
+
destroy() {
|
|
542
|
+
this.stop();
|
|
543
|
+
this.sessionId = null;
|
|
544
|
+
this.sdkSessionToken = null;
|
|
545
|
+
this.events = [];
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/proactive.ts
|
|
550
|
+
var ProactiveBehaviorTracker = class {
|
|
551
|
+
constructor(config, callbacks) {
|
|
552
|
+
this.isTracking = false;
|
|
553
|
+
// Tracking state
|
|
554
|
+
this.scrollDepth = 0;
|
|
555
|
+
this.pageLoadTime = 0;
|
|
556
|
+
this.clickCount = 0;
|
|
557
|
+
this.lastActivityTime = 0;
|
|
558
|
+
this.currentUrl = "";
|
|
559
|
+
this.previousUrl = "";
|
|
560
|
+
// Trigger state
|
|
561
|
+
this.triggeredSet = /* @__PURE__ */ new Set();
|
|
562
|
+
this.lastTriggerTime = 0;
|
|
563
|
+
this.proactiveMessageCount = 0;
|
|
564
|
+
// Listeners
|
|
565
|
+
this.scrollHandler = null;
|
|
566
|
+
this.clickHandler = null;
|
|
567
|
+
this.mouseMoveHandler = null;
|
|
568
|
+
this.mouseLeaveHandler = null;
|
|
569
|
+
this.visibilityHandler = null;
|
|
570
|
+
// Timers
|
|
571
|
+
this.timeCheckInterval = null;
|
|
572
|
+
this.idleCheckInterval = null;
|
|
573
|
+
this.config = config;
|
|
574
|
+
this.callbacks = callbacks;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Start tracking user behavior
|
|
578
|
+
*/
|
|
579
|
+
start() {
|
|
580
|
+
if (!this.config.enabled || this.isTracking) return;
|
|
581
|
+
if (typeof window === "undefined") return;
|
|
582
|
+
if (this.config.respectUserPreference) {
|
|
583
|
+
const optOut = localStorage.getItem("produck_proactive_optout");
|
|
584
|
+
if (optOut === "true") return;
|
|
585
|
+
}
|
|
586
|
+
this.isTracking = true;
|
|
587
|
+
this.pageLoadTime = Date.now();
|
|
588
|
+
this.lastActivityTime = Date.now();
|
|
589
|
+
this.currentUrl = window.location.href;
|
|
590
|
+
this.setupScrollTracking();
|
|
591
|
+
this.setupClickTracking();
|
|
592
|
+
this.setupIdleTracking();
|
|
593
|
+
this.setupExitIntentTracking();
|
|
594
|
+
this.setupTimeTracking();
|
|
595
|
+
this.setupPageChangeTracking();
|
|
596
|
+
console.log("[Produck] Proactive behavior tracking started");
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Stop tracking
|
|
600
|
+
*/
|
|
601
|
+
stop() {
|
|
602
|
+
if (!this.isTracking) return;
|
|
603
|
+
this.isTracking = false;
|
|
604
|
+
if (this.scrollHandler) {
|
|
605
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
606
|
+
}
|
|
607
|
+
if (this.clickHandler) {
|
|
608
|
+
document.removeEventListener("click", this.clickHandler);
|
|
609
|
+
}
|
|
610
|
+
if (this.mouseMoveHandler) {
|
|
611
|
+
document.removeEventListener("mousemove", this.mouseMoveHandler);
|
|
612
|
+
}
|
|
613
|
+
if (this.mouseLeaveHandler) {
|
|
614
|
+
document.removeEventListener("mouseleave", this.mouseLeaveHandler);
|
|
615
|
+
}
|
|
616
|
+
if (this.visibilityHandler) {
|
|
617
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
618
|
+
}
|
|
619
|
+
if (this.timeCheckInterval) {
|
|
620
|
+
clearInterval(this.timeCheckInterval);
|
|
621
|
+
}
|
|
622
|
+
if (this.idleCheckInterval) {
|
|
623
|
+
clearInterval(this.idleCheckInterval);
|
|
624
|
+
}
|
|
625
|
+
console.log("[Produck] Proactive behavior tracking stopped");
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Reset tracking state (e.g., on page change)
|
|
629
|
+
*/
|
|
630
|
+
reset() {
|
|
631
|
+
this.scrollDepth = 0;
|
|
632
|
+
this.clickCount = 0;
|
|
633
|
+
this.pageLoadTime = Date.now();
|
|
634
|
+
this.lastActivityTime = Date.now();
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Get current tracking stats
|
|
638
|
+
*/
|
|
639
|
+
getStats() {
|
|
640
|
+
return {
|
|
641
|
+
scrollDepth: this.scrollDepth,
|
|
642
|
+
timeOnPage: Date.now() - this.pageLoadTime,
|
|
643
|
+
clickCount: this.clickCount,
|
|
644
|
+
idleTime: Date.now() - this.lastActivityTime,
|
|
645
|
+
proactiveMessageCount: this.proactiveMessageCount
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Check if a trigger should fire
|
|
650
|
+
*/
|
|
651
|
+
shouldTrigger(trigger) {
|
|
652
|
+
if (this.config.maxProactiveMessages && this.proactiveMessageCount >= this.config.maxProactiveMessages) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
if (this.config.globalCooldown && Date.now() - this.lastTriggerTime < this.config.globalCooldown) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
const triggerKey = `${trigger.type}-${trigger.threshold || "default"}`;
|
|
659
|
+
if (trigger.onlyOnce && this.triggeredSet.has(triggerKey)) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
const lastTriggerKey = `${triggerKey}-lastTime`;
|
|
663
|
+
const lastTime = parseInt(sessionStorage.getItem(lastTriggerKey) || "0", 10);
|
|
664
|
+
if (trigger.cooldown && Date.now() - lastTime < trigger.cooldown) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Fire a trigger
|
|
671
|
+
*/
|
|
672
|
+
fireTrigger(trigger, context) {
|
|
673
|
+
if (!this.shouldTrigger(trigger)) return;
|
|
674
|
+
const triggerKey = `${trigger.type}-${trigger.threshold || "default"}`;
|
|
675
|
+
const lastTriggerKey = `${triggerKey}-lastTime`;
|
|
676
|
+
this.triggeredSet.add(triggerKey);
|
|
677
|
+
this.lastTriggerTime = Date.now();
|
|
678
|
+
this.proactiveMessageCount++;
|
|
679
|
+
sessionStorage.setItem(lastTriggerKey, Date.now().toString());
|
|
680
|
+
const message = trigger.message || this.getDefaultMessage(trigger.type, context);
|
|
681
|
+
this.callbacks.onTrigger(context, message);
|
|
682
|
+
if (this.callbacks.onMessage) {
|
|
683
|
+
this.callbacks.onMessage(message);
|
|
684
|
+
}
|
|
685
|
+
console.log("[Produck] Proactive trigger fired:", trigger.type, context);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Get default message for trigger type
|
|
689
|
+
*/
|
|
690
|
+
getDefaultMessage(type, context) {
|
|
691
|
+
switch (type) {
|
|
692
|
+
case "scroll":
|
|
693
|
+
return `I see you're exploring the page! Is there something specific you're looking for?`;
|
|
694
|
+
case "time":
|
|
695
|
+
return `You've been here a while. Would you like some help finding what you need?`;
|
|
696
|
+
case "clicks":
|
|
697
|
+
return `Looks like you're navigating around. Can I help you find something?`;
|
|
698
|
+
case "idle":
|
|
699
|
+
return `Still there? Let me know if you have any questions!`;
|
|
700
|
+
case "exit":
|
|
701
|
+
return `Wait! Before you go, is there anything I can help you with?`;
|
|
702
|
+
case "pageChange":
|
|
703
|
+
return `I see you're exploring different pages. Need any guidance?`;
|
|
704
|
+
default:
|
|
705
|
+
return `How can I help you today?`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Setup scroll depth tracking
|
|
710
|
+
*/
|
|
711
|
+
setupScrollTracking() {
|
|
712
|
+
const scrollTriggers = this.config.triggers.filter((t) => t.type === "scroll" && t.enabled);
|
|
713
|
+
if (scrollTriggers.length === 0) return;
|
|
714
|
+
this.scrollHandler = () => {
|
|
715
|
+
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
716
|
+
const scrollPercent = docHeight > 0 ? window.scrollY / docHeight * 100 : 0;
|
|
717
|
+
this.scrollDepth = Math.max(this.scrollDepth, scrollPercent);
|
|
718
|
+
this.lastActivityTime = Date.now();
|
|
719
|
+
scrollTriggers.forEach((trigger) => {
|
|
720
|
+
if (trigger.threshold && this.scrollDepth >= trigger.threshold) {
|
|
721
|
+
this.fireTrigger(trigger, {
|
|
722
|
+
triggerType: "scroll",
|
|
723
|
+
scrollDepth: this.scrollDepth,
|
|
724
|
+
currentUrl: window.location.href,
|
|
725
|
+
pageTitle: document.title
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
window.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Setup click tracking
|
|
734
|
+
*/
|
|
735
|
+
setupClickTracking() {
|
|
736
|
+
const clickTriggers = this.config.triggers.filter((t) => t.type === "clicks" && t.enabled);
|
|
737
|
+
if (clickTriggers.length === 0) return;
|
|
738
|
+
this.clickHandler = () => {
|
|
739
|
+
this.clickCount++;
|
|
740
|
+
this.lastActivityTime = Date.now();
|
|
741
|
+
clickTriggers.forEach((trigger) => {
|
|
742
|
+
if (trigger.threshold && this.clickCount >= trigger.threshold) {
|
|
743
|
+
this.fireTrigger(trigger, {
|
|
744
|
+
triggerType: "clicks",
|
|
745
|
+
clickCount: this.clickCount,
|
|
746
|
+
currentUrl: window.location.href,
|
|
747
|
+
pageTitle: document.title
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
};
|
|
752
|
+
document.addEventListener("click", this.clickHandler);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Setup idle tracking
|
|
756
|
+
*/
|
|
757
|
+
setupIdleTracking() {
|
|
758
|
+
const idleTriggers = this.config.triggers.filter((t) => t.type === "idle" && t.enabled);
|
|
759
|
+
if (idleTriggers.length === 0) return;
|
|
760
|
+
this.mouseMoveHandler = () => {
|
|
761
|
+
this.lastActivityTime = Date.now();
|
|
762
|
+
};
|
|
763
|
+
document.addEventListener("mousemove", this.mouseMoveHandler, { passive: true });
|
|
764
|
+
this.idleCheckInterval = setInterval(() => {
|
|
765
|
+
const idleTime = Date.now() - this.lastActivityTime;
|
|
766
|
+
idleTriggers.forEach((trigger) => {
|
|
767
|
+
if (trigger.threshold && idleTime >= trigger.threshold) {
|
|
768
|
+
this.fireTrigger(trigger, {
|
|
769
|
+
triggerType: "idle",
|
|
770
|
+
idleTime,
|
|
771
|
+
currentUrl: window.location.href,
|
|
772
|
+
pageTitle: document.title
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
}, 1e3);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Setup exit intent tracking
|
|
780
|
+
*/
|
|
781
|
+
setupExitIntentTracking() {
|
|
782
|
+
const exitTriggers = this.config.triggers.filter((t) => t.type === "exit" && t.enabled);
|
|
783
|
+
if (exitTriggers.length === 0) return;
|
|
784
|
+
this.mouseLeaveHandler = (e) => {
|
|
785
|
+
if (e.clientY <= 0) {
|
|
786
|
+
exitTriggers.forEach((trigger) => {
|
|
787
|
+
this.fireTrigger(trigger, {
|
|
788
|
+
triggerType: "exit",
|
|
789
|
+
currentUrl: window.location.href,
|
|
790
|
+
pageTitle: document.title
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
document.addEventListener("mouseleave", this.mouseLeaveHandler);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Setup time on page tracking
|
|
799
|
+
*/
|
|
800
|
+
setupTimeTracking() {
|
|
801
|
+
const timeTriggers = this.config.triggers.filter((t) => t.type === "time" && t.enabled);
|
|
802
|
+
if (timeTriggers.length === 0) return;
|
|
803
|
+
this.timeCheckInterval = setInterval(() => {
|
|
804
|
+
const timeOnPage = Date.now() - this.pageLoadTime;
|
|
805
|
+
timeTriggers.forEach((trigger) => {
|
|
806
|
+
if (trigger.threshold && timeOnPage >= trigger.threshold) {
|
|
807
|
+
this.fireTrigger(trigger, {
|
|
808
|
+
triggerType: "time",
|
|
809
|
+
timeOnPage,
|
|
810
|
+
currentUrl: window.location.href,
|
|
811
|
+
pageTitle: document.title
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}, 1e3);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Setup SPA page change tracking
|
|
819
|
+
*/
|
|
820
|
+
setupPageChangeTracking() {
|
|
821
|
+
const pageTriggers = this.config.triggers.filter((t) => t.type === "pageChange" && t.enabled);
|
|
822
|
+
if (pageTriggers.length === 0) return;
|
|
823
|
+
this.visibilityHandler = () => {
|
|
824
|
+
if (document.visibilityState === "visible") {
|
|
825
|
+
const newUrl = window.location.href;
|
|
826
|
+
if (newUrl !== this.currentUrl) {
|
|
827
|
+
this.previousUrl = this.currentUrl;
|
|
828
|
+
this.currentUrl = newUrl;
|
|
829
|
+
this.scrollDepth = 0;
|
|
830
|
+
this.clickCount = 0;
|
|
831
|
+
this.pageLoadTime = Date.now();
|
|
832
|
+
pageTriggers.forEach((trigger) => {
|
|
833
|
+
this.fireTrigger(trigger, {
|
|
834
|
+
triggerType: "pageChange",
|
|
835
|
+
currentUrl: this.currentUrl,
|
|
836
|
+
previousUrl: this.previousUrl,
|
|
837
|
+
pageTitle: document.title
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
844
|
+
window.addEventListener("popstate", () => {
|
|
845
|
+
const newUrl = window.location.href;
|
|
846
|
+
if (newUrl !== this.currentUrl) {
|
|
847
|
+
this.previousUrl = this.currentUrl;
|
|
848
|
+
this.currentUrl = newUrl;
|
|
849
|
+
pageTriggers.forEach((trigger) => {
|
|
850
|
+
this.fireTrigger(trigger, {
|
|
851
|
+
triggerType: "pageChange",
|
|
852
|
+
currentUrl: this.currentUrl,
|
|
853
|
+
previousUrl: this.previousUrl,
|
|
854
|
+
pageTitle: document.title
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Manually trigger a proactive message (for custom integrations)
|
|
862
|
+
*/
|
|
863
|
+
triggerManual(message, context) {
|
|
864
|
+
this.proactiveMessageCount++;
|
|
865
|
+
this.lastTriggerTime = Date.now();
|
|
866
|
+
const fullContext = {
|
|
867
|
+
triggerType: "time",
|
|
868
|
+
// default
|
|
869
|
+
currentUrl: window.location.href,
|
|
870
|
+
pageTitle: document.title,
|
|
871
|
+
...context
|
|
872
|
+
};
|
|
873
|
+
this.callbacks.onTrigger(fullContext, message);
|
|
874
|
+
if (this.callbacks.onMessage) {
|
|
875
|
+
this.callbacks.onMessage(message);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Opt out of proactive messages
|
|
880
|
+
*/
|
|
881
|
+
optOut() {
|
|
882
|
+
if (typeof localStorage !== "undefined") {
|
|
883
|
+
localStorage.setItem("produck_proactive_optout", "true");
|
|
884
|
+
}
|
|
885
|
+
this.stop();
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Opt in to proactive messages
|
|
889
|
+
*/
|
|
890
|
+
optIn() {
|
|
891
|
+
if (typeof localStorage !== "undefined") {
|
|
892
|
+
localStorage.removeItem("produck_proactive_optout");
|
|
893
|
+
}
|
|
894
|
+
this.start();
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// src/user-flows/utils.ts
|
|
899
|
+
function isBrowser2() {
|
|
900
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
901
|
+
}
|
|
902
|
+
function generateSessionId2() {
|
|
903
|
+
return "ufs_" + Date.now().toString(36) + "_" + Math.random().toString(36).substring(2, 11);
|
|
904
|
+
}
|
|
905
|
+
function getCurrentUrl2() {
|
|
906
|
+
if (!isBrowser2()) return "";
|
|
907
|
+
return window.location.href;
|
|
908
|
+
}
|
|
909
|
+
function getCurrentPath() {
|
|
910
|
+
if (!isBrowser2()) return "";
|
|
911
|
+
return window.location.pathname;
|
|
912
|
+
}
|
|
913
|
+
function getCurrentTitle() {
|
|
914
|
+
if (!isBrowser2()) return "";
|
|
915
|
+
return document.title || "";
|
|
916
|
+
}
|
|
917
|
+
function shouldTrackSession(samplingRate = 1) {
|
|
918
|
+
return Math.random() < samplingRate;
|
|
919
|
+
}
|
|
920
|
+
function getDeviceInfo2() {
|
|
921
|
+
if (!isBrowser2()) {
|
|
922
|
+
return {
|
|
923
|
+
deviceType: "desktop",
|
|
924
|
+
browser: "unknown",
|
|
925
|
+
os: "unknown",
|
|
926
|
+
viewportWidth: 0,
|
|
927
|
+
viewportHeight: 0,
|
|
928
|
+
userAgent: ""
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
const ua = navigator.userAgent;
|
|
932
|
+
let deviceType = "desktop";
|
|
933
|
+
if (/Mobi|Android/i.test(ua)) {
|
|
934
|
+
deviceType = /Tablet|iPad/i.test(ua) ? "tablet" : "mobile";
|
|
935
|
+
}
|
|
936
|
+
let browser = "unknown";
|
|
937
|
+
if (ua.includes("Firefox")) browser = "Firefox";
|
|
938
|
+
else if (ua.includes("Edg")) browser = "Edge";
|
|
939
|
+
else if (ua.includes("Chrome")) browser = "Chrome";
|
|
940
|
+
else if (ua.includes("Safari")) browser = "Safari";
|
|
941
|
+
else if (ua.includes("Opera") || ua.includes("OPR")) browser = "Opera";
|
|
942
|
+
let os = "unknown";
|
|
943
|
+
if (ua.includes("Windows")) os = "Windows";
|
|
944
|
+
else if (ua.includes("Mac OS")) os = "macOS";
|
|
945
|
+
else if (ua.includes("Linux")) os = "Linux";
|
|
946
|
+
else if (ua.includes("Android")) os = "Android";
|
|
947
|
+
else if (ua.includes("iOS") || ua.includes("iPhone") || ua.includes("iPad")) os = "iOS";
|
|
948
|
+
return {
|
|
949
|
+
deviceType,
|
|
950
|
+
browser,
|
|
951
|
+
os,
|
|
952
|
+
viewportWidth: window.innerWidth,
|
|
953
|
+
viewportHeight: window.innerHeight,
|
|
954
|
+
userAgent: ua
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function getElementSelector(element, depth = 5) {
|
|
958
|
+
const parts = [];
|
|
959
|
+
let current = element;
|
|
960
|
+
let currentDepth = 0;
|
|
961
|
+
while (current && current !== document.body && currentDepth < depth) {
|
|
962
|
+
let selector = current.tagName.toLowerCase();
|
|
963
|
+
if (current.id) {
|
|
964
|
+
selector += `#${current.id}`;
|
|
965
|
+
parts.unshift(selector);
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
if (current.className && typeof current.className === "string") {
|
|
969
|
+
const classes = current.className.trim().split(/\s+/);
|
|
970
|
+
if (classes.length > 0 && classes[0]) {
|
|
971
|
+
selector += `.${classes[0]}`;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const parent = current.parentElement;
|
|
975
|
+
if (parent) {
|
|
976
|
+
const siblings = Array.from(parent.children).filter(
|
|
977
|
+
(child) => child.tagName === current.tagName
|
|
978
|
+
);
|
|
979
|
+
if (siblings.length > 1) {
|
|
980
|
+
const index = siblings.indexOf(current) + 1;
|
|
981
|
+
selector += `:nth-child(${index})`;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
parts.unshift(selector);
|
|
985
|
+
current = current.parentElement;
|
|
986
|
+
currentDepth++;
|
|
987
|
+
}
|
|
988
|
+
return parts.join(" > ");
|
|
989
|
+
}
|
|
990
|
+
function getElementXPath(element) {
|
|
991
|
+
if (!element) return "";
|
|
992
|
+
if (element.id) {
|
|
993
|
+
return `//*[@id="${element.id}"]`;
|
|
994
|
+
}
|
|
995
|
+
const parts = [];
|
|
996
|
+
let current = element;
|
|
997
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
998
|
+
let index = 0;
|
|
999
|
+
let sibling = current.previousElementSibling;
|
|
1000
|
+
while (sibling) {
|
|
1001
|
+
if (sibling.tagName === current.tagName) {
|
|
1002
|
+
index++;
|
|
1003
|
+
}
|
|
1004
|
+
sibling = sibling.previousElementSibling;
|
|
1005
|
+
}
|
|
1006
|
+
const tagName = current.tagName.toLowerCase();
|
|
1007
|
+
const part = index > 0 ? `${tagName}[${index + 1}]` : tagName;
|
|
1008
|
+
parts.unshift(part);
|
|
1009
|
+
current = current.parentElement;
|
|
1010
|
+
}
|
|
1011
|
+
return "/" + parts.join("/");
|
|
1012
|
+
}
|
|
1013
|
+
function getElementInfo(element, config = {}) {
|
|
1014
|
+
const maxTextLength = config?.maxTextLength || 100;
|
|
1015
|
+
const maxHtmlLength = config?.maxHtmlLength || 500;
|
|
1016
|
+
const selectorDepth = config?.selectorDepth || 5;
|
|
1017
|
+
let text = "";
|
|
1018
|
+
if (element instanceof HTMLElement) {
|
|
1019
|
+
text = element.innerText || element.textContent || "";
|
|
1020
|
+
text = text.trim().substring(0, maxTextLength);
|
|
1021
|
+
if (text.length === maxTextLength) text += "...";
|
|
1022
|
+
}
|
|
1023
|
+
let html;
|
|
1024
|
+
if (config?.captureHtml !== false) {
|
|
1025
|
+
html = element.outerHTML.substring(0, maxHtmlLength);
|
|
1026
|
+
if (html.length === maxHtmlLength) html += "...";
|
|
1027
|
+
}
|
|
1028
|
+
const rect = element.getBoundingClientRect();
|
|
1029
|
+
const info = {
|
|
1030
|
+
tag: element.tagName.toLowerCase(),
|
|
1031
|
+
selector: getElementSelector(element, selectorDepth)
|
|
1032
|
+
};
|
|
1033
|
+
if (element.id) info.id = element.id;
|
|
1034
|
+
if (element.className && typeof element.className === "string") {
|
|
1035
|
+
info.className = element.className;
|
|
1036
|
+
}
|
|
1037
|
+
if (text) info.text = text;
|
|
1038
|
+
if (element instanceof HTMLAnchorElement && element.href) {
|
|
1039
|
+
info.href = element.href;
|
|
1040
|
+
}
|
|
1041
|
+
info.xpath = getElementXPath(element);
|
|
1042
|
+
info.rect = {
|
|
1043
|
+
x: Math.round(rect.x),
|
|
1044
|
+
y: Math.round(rect.y),
|
|
1045
|
+
width: Math.round(rect.width),
|
|
1046
|
+
height: Math.round(rect.height)
|
|
1047
|
+
};
|
|
1048
|
+
if (html) info.html = html;
|
|
1049
|
+
return info;
|
|
1050
|
+
}
|
|
1051
|
+
function matchesSelectors(element, selectors) {
|
|
1052
|
+
if (!selectors || selectors.length === 0) return false;
|
|
1053
|
+
return selectors.some((selector) => {
|
|
1054
|
+
try {
|
|
1055
|
+
return element.matches(selector) || element.closest(selector) !== null;
|
|
1056
|
+
} catch {
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
function captureElementVisualInfo(element) {
|
|
1062
|
+
try {
|
|
1063
|
+
if (typeof window === "undefined") return void 0;
|
|
1064
|
+
const htmlElement = element;
|
|
1065
|
+
const styles = window.getComputedStyle(htmlElement);
|
|
1066
|
+
return {
|
|
1067
|
+
backgroundColor: styles.backgroundColor,
|
|
1068
|
+
color: styles.color,
|
|
1069
|
+
fontSize: styles.fontSize,
|
|
1070
|
+
fontWeight: styles.fontWeight,
|
|
1071
|
+
borderRadius: styles.borderRadius,
|
|
1072
|
+
border: styles.border,
|
|
1073
|
+
boxShadow: styles.boxShadow,
|
|
1074
|
+
padding: styles.padding
|
|
1075
|
+
};
|
|
1076
|
+
} catch {
|
|
1077
|
+
return void 0;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async function captureElementScreenshot(element, maxSize = 200) {
|
|
1081
|
+
try {
|
|
1082
|
+
if (typeof window === "undefined") return void 0;
|
|
1083
|
+
const html2canvas = window.html2canvas;
|
|
1084
|
+
if (!html2canvas) {
|
|
1085
|
+
try {
|
|
1086
|
+
const module2 = await import(
|
|
1087
|
+
/* webpackIgnore: true */
|
|
1088
|
+
"html2canvas"
|
|
1089
|
+
);
|
|
1090
|
+
const canvas2 = await module2.default(element, {
|
|
1091
|
+
scale: 0.5,
|
|
1092
|
+
width: maxSize,
|
|
1093
|
+
height: maxSize,
|
|
1094
|
+
useCORS: true,
|
|
1095
|
+
logging: false
|
|
1096
|
+
});
|
|
1097
|
+
return canvas2.toDataURL("image/jpeg", 0.7);
|
|
1098
|
+
} catch {
|
|
1099
|
+
console.debug("[UserFlow] html2canvas not available for screenshots");
|
|
1100
|
+
return void 0;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const canvas = await html2canvas(element, {
|
|
1104
|
+
scale: 0.5,
|
|
1105
|
+
width: maxSize,
|
|
1106
|
+
height: maxSize,
|
|
1107
|
+
useCORS: true,
|
|
1108
|
+
logging: false
|
|
1109
|
+
});
|
|
1110
|
+
return canvas.toDataURL("image/jpeg", 0.7);
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
console.debug("[UserFlow] Failed to capture screenshot:", error);
|
|
1113
|
+
return void 0;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function mergeConfig2(config) {
|
|
1117
|
+
const defaults = {
|
|
1118
|
+
enabled: false,
|
|
1119
|
+
samplingRate: 1,
|
|
1120
|
+
events: {
|
|
1121
|
+
clicks: true,
|
|
1122
|
+
navigation: true,
|
|
1123
|
+
formSubmit: true,
|
|
1124
|
+
inputs: false,
|
|
1125
|
+
scroll: false,
|
|
1126
|
+
ignoreSelectors: [],
|
|
1127
|
+
trackOnlySelectors: []
|
|
1128
|
+
},
|
|
1129
|
+
element: {
|
|
1130
|
+
captureVisualStyles: true,
|
|
1131
|
+
// Lightweight - enabled by default
|
|
1132
|
+
captureScreenshot: false,
|
|
1133
|
+
// Expensive - disabled by default
|
|
1134
|
+
screenshotMaxSize: 200,
|
|
1135
|
+
captureHtml: true,
|
|
1136
|
+
maxHtmlLength: 500,
|
|
1137
|
+
maxTextLength: 100,
|
|
1138
|
+
selectorDepth: 5
|
|
1139
|
+
},
|
|
1140
|
+
performance: {
|
|
1141
|
+
batchEvents: true,
|
|
1142
|
+
batchSize: 10,
|
|
1143
|
+
batchTimeout: 5e3,
|
|
1144
|
+
clickDebounce: 500
|
|
1145
|
+
},
|
|
1146
|
+
session: {
|
|
1147
|
+
endOnHidden: true,
|
|
1148
|
+
inactivityTimeout: 5 * 60 * 1e3,
|
|
1149
|
+
// 5 minutes
|
|
1150
|
+
endOnExternalNavigation: true,
|
|
1151
|
+
autoRestart: true
|
|
1152
|
+
},
|
|
1153
|
+
endpoint: "",
|
|
1154
|
+
onStart: () => {
|
|
1155
|
+
},
|
|
1156
|
+
onStop: () => {
|
|
1157
|
+
},
|
|
1158
|
+
onError: () => {
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
if (!config) return defaults;
|
|
1162
|
+
return {
|
|
1163
|
+
enabled: config.enabled ?? defaults.enabled,
|
|
1164
|
+
samplingRate: config.samplingRate ?? defaults.samplingRate,
|
|
1165
|
+
events: { ...defaults.events, ...config.events },
|
|
1166
|
+
element: { ...defaults.element, ...config.element },
|
|
1167
|
+
performance: { ...defaults.performance, ...config.performance },
|
|
1168
|
+
session: { ...defaults.session, ...config.session },
|
|
1169
|
+
endpoint: config.endpoint ?? defaults.endpoint,
|
|
1170
|
+
onStart: config.onStart ?? defaults.onStart,
|
|
1171
|
+
onStop: config.onStop ?? defaults.onStop,
|
|
1172
|
+
onError: config.onError ?? defaults.onError
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/user-flows/tracker.ts
|
|
1177
|
+
var UserFlowTracker = class {
|
|
1178
|
+
constructor(config) {
|
|
1179
|
+
this.state = "idle";
|
|
1180
|
+
this.sessionId = null;
|
|
1181
|
+
this.sdkSessionToken = null;
|
|
1182
|
+
this.apiUrl = "";
|
|
1183
|
+
this.sdkKey = null;
|
|
1184
|
+
this.user = null;
|
|
1185
|
+
// Tracking internals
|
|
1186
|
+
this.events = [];
|
|
1187
|
+
this.eventCount = 0;
|
|
1188
|
+
this.batchIndex = 0;
|
|
1189
|
+
this.batchesSent = 0;
|
|
1190
|
+
this.pageCount = 0;
|
|
1191
|
+
this.startTime = null;
|
|
1192
|
+
this.batchTimeout = null;
|
|
1193
|
+
this.inactivityTimeout = null;
|
|
1194
|
+
this.lastActivityTime = Date.now();
|
|
1195
|
+
this.lastClickTime = 0;
|
|
1196
|
+
this.lastClickElement = null;
|
|
1197
|
+
this.currentPath = "";
|
|
1198
|
+
this.visitedPaths = /* @__PURE__ */ new Set();
|
|
1199
|
+
// Event listeners
|
|
1200
|
+
this.clickHandler = null;
|
|
1201
|
+
this.navigationHandler = null;
|
|
1202
|
+
this.formSubmitHandler = null;
|
|
1203
|
+
this.beforeUnloadHandler = null;
|
|
1204
|
+
this.visibilityHandler = null;
|
|
1205
|
+
// Linked recording session ID (if rrweb is also recording)
|
|
1206
|
+
this.linkedRecordingSessionId = null;
|
|
1207
|
+
this.config = mergeConfig2(config);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Initialize the tracker with SDK context
|
|
1211
|
+
*/
|
|
1212
|
+
initialize(sdkSessionToken, apiUrl, sdkKey) {
|
|
1213
|
+
this.sdkSessionToken = sdkSessionToken;
|
|
1214
|
+
this.apiUrl = apiUrl;
|
|
1215
|
+
this.sdkKey = sdkKey || null;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Update configuration
|
|
1219
|
+
*/
|
|
1220
|
+
updateConfig(config) {
|
|
1221
|
+
this.config = mergeConfig2({ ...this.config, ...config });
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Identify the user
|
|
1225
|
+
*/
|
|
1226
|
+
identify(user) {
|
|
1227
|
+
this.user = user;
|
|
1228
|
+
if (this.state === "tracking" && this.sessionId) {
|
|
1229
|
+
this.sendIdentificationUpdate().catch((err) => {
|
|
1230
|
+
console.error("[UserFlow] Failed to send identification update:", err);
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Start tracking
|
|
1236
|
+
* @param linkedRecordingSessionId - Optional linked rrweb session ID for playback
|
|
1237
|
+
*/
|
|
1238
|
+
async start(linkedRecordingSessionId) {
|
|
1239
|
+
if (!isBrowser2()) {
|
|
1240
|
+
console.warn("[UserFlow] Cannot track outside browser environment");
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
if (!this.config.enabled) {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
if (this.state === "tracking") {
|
|
1247
|
+
console.warn("[UserFlow] Already tracking");
|
|
1248
|
+
return this.sessionId;
|
|
1249
|
+
}
|
|
1250
|
+
if (!shouldTrackSession(this.config.samplingRate)) {
|
|
1251
|
+
console.log("[UserFlow] Session not sampled for tracking");
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
this.sessionId = generateSessionId2();
|
|
1256
|
+
this.linkedRecordingSessionId = linkedRecordingSessionId || null;
|
|
1257
|
+
this.events = [];
|
|
1258
|
+
this.eventCount = 0;
|
|
1259
|
+
this.batchIndex = 0;
|
|
1260
|
+
this.batchesSent = 0;
|
|
1261
|
+
this.pageCount = 1;
|
|
1262
|
+
this.startTime = /* @__PURE__ */ new Date();
|
|
1263
|
+
this.currentPath = getCurrentPath();
|
|
1264
|
+
this.visitedPaths = /* @__PURE__ */ new Set([this.currentPath]);
|
|
1265
|
+
this.state = "tracking";
|
|
1266
|
+
this.setupEventListeners();
|
|
1267
|
+
await this.sendSessionStart();
|
|
1268
|
+
this.addEvent({
|
|
1269
|
+
eventType: "navigation",
|
|
1270
|
+
eventOrder: 0,
|
|
1271
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1272
|
+
pageUrl: getCurrentUrl2(),
|
|
1273
|
+
pageTitle: getCurrentTitle(),
|
|
1274
|
+
pagePath: this.currentPath
|
|
1275
|
+
});
|
|
1276
|
+
this.config.onStart(this.sessionId);
|
|
1277
|
+
console.log("[UserFlow] Started tracking session:", this.sessionId);
|
|
1278
|
+
return this.sessionId;
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
console.error("[UserFlow] Failed to start tracking:", error);
|
|
1281
|
+
this.state = "idle";
|
|
1282
|
+
this.config.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Stop tracking
|
|
1288
|
+
*/
|
|
1289
|
+
async stop() {
|
|
1290
|
+
if (this.state !== "tracking" && this.state !== "paused") {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
this.removeEventListeners();
|
|
1295
|
+
if (this.batchTimeout) {
|
|
1296
|
+
clearTimeout(this.batchTimeout);
|
|
1297
|
+
this.batchTimeout = null;
|
|
1298
|
+
}
|
|
1299
|
+
if (this.inactivityTimeout) {
|
|
1300
|
+
clearTimeout(this.inactivityTimeout);
|
|
1301
|
+
this.inactivityTimeout = null;
|
|
1302
|
+
}
|
|
1303
|
+
if (this.events.length > 0) {
|
|
1304
|
+
await this.sendEventBatch(true);
|
|
1305
|
+
}
|
|
1306
|
+
await this.sendSessionEnd();
|
|
1307
|
+
this.state = "stopped";
|
|
1308
|
+
const eventCount = this.eventCount;
|
|
1309
|
+
const sessionId = this.sessionId;
|
|
1310
|
+
this.sessionId = null;
|
|
1311
|
+
this.events = [];
|
|
1312
|
+
this.eventCount = 0;
|
|
1313
|
+
this.startTime = null;
|
|
1314
|
+
this.config.onStop(sessionId, eventCount);
|
|
1315
|
+
console.log("[UserFlow] Stopped tracking session:", sessionId);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
console.error("[UserFlow] Error stopping tracking:", error);
|
|
1318
|
+
this.state = "stopped";
|
|
1319
|
+
this.config.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Pause tracking
|
|
1324
|
+
*/
|
|
1325
|
+
pause() {
|
|
1326
|
+
if (this.state === "tracking") {
|
|
1327
|
+
this.state = "paused";
|
|
1328
|
+
console.log("[UserFlow] Tracking paused");
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Resume tracking
|
|
1333
|
+
*/
|
|
1334
|
+
resume() {
|
|
1335
|
+
if (this.state === "paused") {
|
|
1336
|
+
this.state = "tracking";
|
|
1337
|
+
console.log("[UserFlow] Tracking resumed");
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Get tracker statistics
|
|
1342
|
+
*/
|
|
1343
|
+
getStats() {
|
|
1344
|
+
return {
|
|
1345
|
+
sessionId: this.sessionId,
|
|
1346
|
+
state: this.state,
|
|
1347
|
+
eventCount: this.eventCount,
|
|
1348
|
+
pageCount: this.pageCount,
|
|
1349
|
+
batchesSent: this.batchesSent,
|
|
1350
|
+
duration: this.startTime ? Date.now() - this.startTime.getTime() : 0,
|
|
1351
|
+
startTime: this.startTime
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Get tracking state
|
|
1356
|
+
*/
|
|
1357
|
+
getState() {
|
|
1358
|
+
return this.state;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Check if currently tracking
|
|
1362
|
+
*/
|
|
1363
|
+
isTracking() {
|
|
1364
|
+
return this.state === "tracking";
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Add a custom event
|
|
1368
|
+
*/
|
|
1369
|
+
addCustomEvent(data) {
|
|
1370
|
+
if (this.state !== "tracking") return;
|
|
1371
|
+
this.addEvent({
|
|
1372
|
+
eventType: "custom",
|
|
1373
|
+
eventOrder: this.eventCount,
|
|
1374
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1375
|
+
pageUrl: getCurrentUrl2(),
|
|
1376
|
+
pageTitle: getCurrentTitle(),
|
|
1377
|
+
pagePath: getCurrentPath(),
|
|
1378
|
+
metadata: data
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
// === Private methods ===
|
|
1382
|
+
/**
|
|
1383
|
+
* Setup event listeners
|
|
1384
|
+
*/
|
|
1385
|
+
setupEventListeners() {
|
|
1386
|
+
if (!isBrowser2()) return;
|
|
1387
|
+
if (this.config.events.clicks) {
|
|
1388
|
+
this.clickHandler = (e) => this.handleClick(e);
|
|
1389
|
+
document.addEventListener("click", this.clickHandler, { capture: true, passive: true });
|
|
1390
|
+
}
|
|
1391
|
+
if (this.config.events.navigation) {
|
|
1392
|
+
this.navigationHandler = () => this.handleNavigation();
|
|
1393
|
+
window.addEventListener("popstate", this.navigationHandler);
|
|
1394
|
+
const originalPushState = history.pushState;
|
|
1395
|
+
const originalReplaceState = history.replaceState;
|
|
1396
|
+
history.pushState = (...args) => {
|
|
1397
|
+
originalPushState.apply(history, args);
|
|
1398
|
+
this.handleNavigation();
|
|
1399
|
+
};
|
|
1400
|
+
history.replaceState = (...args) => {
|
|
1401
|
+
originalReplaceState.apply(history, args);
|
|
1402
|
+
this.handleNavigation();
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
if (this.config.events.formSubmit) {
|
|
1406
|
+
this.formSubmitHandler = (e) => this.handleFormSubmit(e);
|
|
1407
|
+
document.addEventListener("submit", this.formSubmitHandler, { capture: true });
|
|
1408
|
+
}
|
|
1409
|
+
this.beforeUnloadHandler = () => {
|
|
1410
|
+
if (this.events.length > 0) {
|
|
1411
|
+
this.sendEventBatchSync(true);
|
|
1412
|
+
}
|
|
1413
|
+
this.sendSessionEndSync();
|
|
1414
|
+
};
|
|
1415
|
+
window.addEventListener("beforeunload", this.beforeUnloadHandler);
|
|
1416
|
+
this.visibilityHandler = () => {
|
|
1417
|
+
if (document.visibilityState === "hidden") {
|
|
1418
|
+
if (this.events.length > 0) {
|
|
1419
|
+
this.sendEventBatch(true).catch(console.error);
|
|
1420
|
+
}
|
|
1421
|
+
if (this.config.session.endOnHidden) {
|
|
1422
|
+
this.stop().catch(console.error);
|
|
1423
|
+
}
|
|
1424
|
+
} else if (document.visibilityState === "visible") {
|
|
1425
|
+
if (this.config.session.autoRestart && this.state === "stopped") {
|
|
1426
|
+
this.start().catch(console.error);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
1431
|
+
this.resetInactivityTimeout();
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Remove event listeners
|
|
1435
|
+
*/
|
|
1436
|
+
removeEventListeners() {
|
|
1437
|
+
if (!isBrowser2()) return;
|
|
1438
|
+
if (this.clickHandler) {
|
|
1439
|
+
document.removeEventListener("click", this.clickHandler, { capture: true });
|
|
1440
|
+
this.clickHandler = null;
|
|
1441
|
+
}
|
|
1442
|
+
if (this.navigationHandler) {
|
|
1443
|
+
window.removeEventListener("popstate", this.navigationHandler);
|
|
1444
|
+
this.navigationHandler = null;
|
|
1445
|
+
}
|
|
1446
|
+
if (this.formSubmitHandler) {
|
|
1447
|
+
document.removeEventListener("submit", this.formSubmitHandler, { capture: true });
|
|
1448
|
+
this.formSubmitHandler = null;
|
|
1449
|
+
}
|
|
1450
|
+
if (this.beforeUnloadHandler) {
|
|
1451
|
+
window.removeEventListener("beforeunload", this.beforeUnloadHandler);
|
|
1452
|
+
this.beforeUnloadHandler = null;
|
|
1453
|
+
}
|
|
1454
|
+
if (this.visibilityHandler) {
|
|
1455
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
1456
|
+
this.visibilityHandler = null;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Handle click event
|
|
1461
|
+
*/
|
|
1462
|
+
async handleClick(e) {
|
|
1463
|
+
if (this.state !== "tracking") return;
|
|
1464
|
+
const target = e.target;
|
|
1465
|
+
if (!target || !(target instanceof Element)) return;
|
|
1466
|
+
if (this.config.events.ignoreSelectors && this.config.events.ignoreSelectors.length > 0 && matchesSelectors(target, this.config.events.ignoreSelectors)) {
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (this.config.events.trackOnlySelectors && this.config.events.trackOnlySelectors.length > 0 && !matchesSelectors(target, this.config.events.trackOnlySelectors)) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const now = Date.now();
|
|
1473
|
+
if (this.lastClickElement === target && now - this.lastClickTime < this.config.performance.clickDebounce) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
this.lastClickTime = now;
|
|
1477
|
+
this.lastClickElement = target;
|
|
1478
|
+
const elementInfo = getElementInfo(target, this.config.element);
|
|
1479
|
+
if (this.config.element.captureVisualStyles) {
|
|
1480
|
+
elementInfo.visualStyles = captureElementVisualInfo(target);
|
|
1481
|
+
}
|
|
1482
|
+
if (this.config.element.captureScreenshot) {
|
|
1483
|
+
try {
|
|
1484
|
+
elementInfo.screenshot = await captureElementScreenshot(
|
|
1485
|
+
target,
|
|
1486
|
+
this.config.element.screenshotMaxSize
|
|
1487
|
+
);
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const event = {
|
|
1492
|
+
eventType: "click",
|
|
1493
|
+
eventOrder: this.eventCount,
|
|
1494
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1495
|
+
pageUrl: getCurrentUrl2(),
|
|
1496
|
+
pageTitle: getCurrentTitle(),
|
|
1497
|
+
pagePath: getCurrentPath(),
|
|
1498
|
+
element: elementInfo,
|
|
1499
|
+
clickX: Math.round(e.pageX),
|
|
1500
|
+
clickY: Math.round(e.pageY),
|
|
1501
|
+
viewportX: Math.round(e.clientX),
|
|
1502
|
+
viewportY: Math.round(e.clientY)
|
|
1503
|
+
};
|
|
1504
|
+
this.addEvent(event);
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Handle navigation event
|
|
1508
|
+
*/
|
|
1509
|
+
handleNavigation() {
|
|
1510
|
+
if (this.state !== "tracking") return;
|
|
1511
|
+
const newPath = getCurrentPath();
|
|
1512
|
+
if (newPath === this.currentPath) return;
|
|
1513
|
+
this.currentPath = newPath;
|
|
1514
|
+
if (!this.visitedPaths.has(newPath)) {
|
|
1515
|
+
this.visitedPaths.add(newPath);
|
|
1516
|
+
this.pageCount++;
|
|
1517
|
+
}
|
|
1518
|
+
const event = {
|
|
1519
|
+
eventType: "navigation",
|
|
1520
|
+
eventOrder: this.eventCount,
|
|
1521
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1522
|
+
pageUrl: getCurrentUrl2(),
|
|
1523
|
+
pageTitle: getCurrentTitle(),
|
|
1524
|
+
pagePath: newPath
|
|
1525
|
+
};
|
|
1526
|
+
this.addEvent(event);
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Handle form submit event
|
|
1530
|
+
*/
|
|
1531
|
+
handleFormSubmit(e) {
|
|
1532
|
+
if (this.state !== "tracking") return;
|
|
1533
|
+
const form = e.target;
|
|
1534
|
+
if (!form || !(form instanceof HTMLFormElement)) return;
|
|
1535
|
+
if (this.config.events.ignoreSelectors && matchesSelectors(form, this.config.events.ignoreSelectors)) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const elementInfo = getElementInfo(form, this.config.element);
|
|
1539
|
+
const event = {
|
|
1540
|
+
eventType: "form_submit",
|
|
1541
|
+
eventOrder: this.eventCount,
|
|
1542
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1543
|
+
pageUrl: getCurrentUrl2(),
|
|
1544
|
+
pageTitle: getCurrentTitle(),
|
|
1545
|
+
pagePath: getCurrentPath(),
|
|
1546
|
+
element: elementInfo,
|
|
1547
|
+
metadata: {
|
|
1548
|
+
action: form.action,
|
|
1549
|
+
method: form.method,
|
|
1550
|
+
name: form.name
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
this.addEvent(event);
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Add event to queue
|
|
1557
|
+
*/
|
|
1558
|
+
addEvent(event) {
|
|
1559
|
+
this.events.push(event);
|
|
1560
|
+
this.eventCount++;
|
|
1561
|
+
this.resetInactivityTimeout();
|
|
1562
|
+
if (this.config.performance.batchEvents && this.events.length >= this.config.performance.batchSize) {
|
|
1563
|
+
this.sendEventBatch(false).catch(console.error);
|
|
1564
|
+
} else if (!this.batchTimeout) {
|
|
1565
|
+
this.batchTimeout = setTimeout(() => {
|
|
1566
|
+
this.batchTimeout = null;
|
|
1567
|
+
if (this.events.length > 0) {
|
|
1568
|
+
this.sendEventBatch(false).catch(console.error);
|
|
1569
|
+
}
|
|
1570
|
+
}, this.config.performance.batchTimeout);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Send session start to backend
|
|
1575
|
+
*/
|
|
1576
|
+
async sendSessionStart() {
|
|
1577
|
+
const endpoint = this.config.endpoint || `${this.apiUrl}/user-flows/session/start`;
|
|
1578
|
+
const device = getDeviceInfo2();
|
|
1579
|
+
const body = {
|
|
1580
|
+
sessionId: this.sessionId,
|
|
1581
|
+
sdkSessionToken: this.sdkSessionToken,
|
|
1582
|
+
startUrl: getCurrentUrl2(),
|
|
1583
|
+
startTime: this.startTime?.toISOString(),
|
|
1584
|
+
user: this.user,
|
|
1585
|
+
device,
|
|
1586
|
+
// Link to rrweb recording session if available
|
|
1587
|
+
linkedRecordingSessionId: this.linkedRecordingSessionId
|
|
1588
|
+
};
|
|
1589
|
+
const response = await fetch(endpoint, {
|
|
1590
|
+
method: "POST",
|
|
1591
|
+
headers: {
|
|
1592
|
+
"Content-Type": "application/json",
|
|
1593
|
+
...this.sdkKey ? { "X-SDK-Key": this.sdkKey } : {}
|
|
1594
|
+
},
|
|
1595
|
+
body: JSON.stringify(body)
|
|
1596
|
+
});
|
|
1597
|
+
if (!response.ok) {
|
|
1598
|
+
throw new Error(`Failed to start session: ${response.status}`);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Send event batch to backend
|
|
1603
|
+
*/
|
|
1604
|
+
async sendEventBatch(isFinal) {
|
|
1605
|
+
if (this.events.length === 0) return;
|
|
1606
|
+
const eventsToSend = [...this.events];
|
|
1607
|
+
this.events = [];
|
|
1608
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/events` : `${this.apiUrl}/user-flows/session/events`;
|
|
1609
|
+
const body = {
|
|
1610
|
+
sessionId: this.sessionId,
|
|
1611
|
+
events: eventsToSend,
|
|
1612
|
+
batchIndex: this.batchIndex++,
|
|
1613
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1614
|
+
isFinal
|
|
1615
|
+
};
|
|
1616
|
+
try {
|
|
1617
|
+
const response = await fetch(endpoint, {
|
|
1618
|
+
method: "POST",
|
|
1619
|
+
headers: {
|
|
1620
|
+
"Content-Type": "application/json",
|
|
1621
|
+
...this.sdkKey ? { "X-SDK-Key": this.sdkKey } : {}
|
|
1622
|
+
},
|
|
1623
|
+
body: JSON.stringify(body)
|
|
1624
|
+
});
|
|
1625
|
+
if (response.ok) {
|
|
1626
|
+
this.batchesSent++;
|
|
1627
|
+
}
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
console.error("[UserFlow] Failed to send event batch:", error);
|
|
1630
|
+
this.events = [...eventsToSend, ...this.events];
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Send event batch synchronously (for beforeunload)
|
|
1635
|
+
*/
|
|
1636
|
+
sendEventBatchSync(isFinal) {
|
|
1637
|
+
if (this.events.length === 0) return;
|
|
1638
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/events` : `${this.apiUrl}/user-flows/session/events`;
|
|
1639
|
+
const body = {
|
|
1640
|
+
sessionId: this.sessionId,
|
|
1641
|
+
events: this.events,
|
|
1642
|
+
batchIndex: this.batchIndex++,
|
|
1643
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1644
|
+
isFinal
|
|
1645
|
+
};
|
|
1646
|
+
if (navigator.sendBeacon) {
|
|
1647
|
+
const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
|
|
1648
|
+
navigator.sendBeacon(endpoint, blob);
|
|
1649
|
+
this.events = [];
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Send session end to backend
|
|
1654
|
+
*/
|
|
1655
|
+
async sendSessionEnd() {
|
|
1656
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/end` : `${this.apiUrl}/user-flows/session/end`;
|
|
1657
|
+
const body = {
|
|
1658
|
+
sessionId: this.sessionId,
|
|
1659
|
+
endTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1660
|
+
eventCount: this.eventCount,
|
|
1661
|
+
status: "completed"
|
|
1662
|
+
};
|
|
1663
|
+
try {
|
|
1664
|
+
await fetch(endpoint, {
|
|
1665
|
+
method: "POST",
|
|
1666
|
+
headers: {
|
|
1667
|
+
"Content-Type": "application/json",
|
|
1668
|
+
...this.sdkKey ? { "X-SDK-Key": this.sdkKey } : {}
|
|
1669
|
+
},
|
|
1670
|
+
body: JSON.stringify(body)
|
|
1671
|
+
});
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
console.error("[UserFlow] Failed to send session end:", error);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Send session end synchronously (for beforeunload)
|
|
1678
|
+
*/
|
|
1679
|
+
sendSessionEndSync() {
|
|
1680
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/end` : `${this.apiUrl}/user-flows/session/end`;
|
|
1681
|
+
const body = {
|
|
1682
|
+
sessionId: this.sessionId,
|
|
1683
|
+
endTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1684
|
+
eventCount: this.eventCount,
|
|
1685
|
+
status: "completed"
|
|
1686
|
+
};
|
|
1687
|
+
if (navigator.sendBeacon) {
|
|
1688
|
+
const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
|
|
1689
|
+
navigator.sendBeacon(endpoint, blob);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Reset inactivity timeout
|
|
1694
|
+
* Called on each user interaction
|
|
1695
|
+
*/
|
|
1696
|
+
resetInactivityTimeout() {
|
|
1697
|
+
if (this.inactivityTimeout) {
|
|
1698
|
+
clearTimeout(this.inactivityTimeout);
|
|
1699
|
+
this.inactivityTimeout = null;
|
|
1700
|
+
}
|
|
1701
|
+
this.lastActivityTime = Date.now();
|
|
1702
|
+
const timeout = this.config.session.inactivityTimeout;
|
|
1703
|
+
if (timeout && timeout > 0) {
|
|
1704
|
+
this.inactivityTimeout = setTimeout(() => {
|
|
1705
|
+
if (this.state === "tracking") {
|
|
1706
|
+
console.log("[UserFlow] Session ended due to inactivity");
|
|
1707
|
+
this.stop().catch(console.error);
|
|
1708
|
+
}
|
|
1709
|
+
}, timeout);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Send identification update to backend
|
|
1714
|
+
*/
|
|
1715
|
+
async sendIdentificationUpdate() {
|
|
1716
|
+
if (!this.sessionId || !this.user) return;
|
|
1717
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/identify` : `${this.apiUrl}/user-flows/session/identify`;
|
|
1718
|
+
const body = {
|
|
1719
|
+
sessionId: this.sessionId,
|
|
1720
|
+
user: this.user
|
|
1721
|
+
};
|
|
1722
|
+
await fetch(endpoint, {
|
|
1723
|
+
method: "PATCH",
|
|
1724
|
+
headers: {
|
|
1725
|
+
"Content-Type": "application/json",
|
|
1726
|
+
...this.sdkKey ? { "X-SDK-Key": this.sdkKey } : {}
|
|
1727
|
+
},
|
|
1728
|
+
body: JSON.stringify(body)
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
|
|
49
1733
|
// src/core.ts
|
|
50
1734
|
var ProduckSDK = class {
|
|
51
1735
|
constructor(config) {
|
|
@@ -53,6 +1737,12 @@ var ProduckSDK = class {
|
|
|
53
1737
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
54
1738
|
this.sessionToken = null;
|
|
55
1739
|
this.isReady = false;
|
|
1740
|
+
/** Session recorder instance (optional, isolated module) */
|
|
1741
|
+
this.recorder = null;
|
|
1742
|
+
/** Proactive behavior tracker instance (optional, isolated module) */
|
|
1743
|
+
this.proactiveTracker = null;
|
|
1744
|
+
/** User flow tracker instance (optional, isolated module) */
|
|
1745
|
+
this.userFlowTracker = null;
|
|
56
1746
|
let apiUrl = config.apiUrl;
|
|
57
1747
|
if (!apiUrl) {
|
|
58
1748
|
if (typeof window !== "undefined") {
|
|
@@ -61,458 +1751,852 @@ var ProduckSDK = class {
|
|
|
61
1751
|
apiUrl = "http://localhost:4001/api/v1";
|
|
62
1752
|
}
|
|
63
1753
|
}
|
|
64
|
-
this.config = {
|
|
65
|
-
apiUrl,
|
|
66
|
-
...config
|
|
1754
|
+
this.config = {
|
|
1755
|
+
apiUrl,
|
|
1756
|
+
...config
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Initialize the SDK and create a chat session
|
|
1761
|
+
*/
|
|
1762
|
+
async init() {
|
|
1763
|
+
await this.log("info", "SDK Initializing", {
|
|
1764
|
+
apiUrl: this.config.apiUrl,
|
|
1765
|
+
hasSDKKey: !!this.config.sdkKey,
|
|
1766
|
+
hasGuiderId: !!this.config.guiderId
|
|
1767
|
+
});
|
|
1768
|
+
try {
|
|
1769
|
+
let endpoint;
|
|
1770
|
+
if (this.config.sdkKey) {
|
|
1771
|
+
endpoint = `${this.config.apiUrl}/sdk/session`;
|
|
1772
|
+
await this.log("info", "Using SDK key authentication");
|
|
1773
|
+
} else if (this.config.guiderId) {
|
|
1774
|
+
endpoint = `${this.config.apiUrl}/chat/${this.config.guiderId}/session`;
|
|
1775
|
+
await this.log("info", "Using guider ID authentication");
|
|
1776
|
+
} else {
|
|
1777
|
+
throw new Error("Either sdkKey or guiderId must be provided");
|
|
1778
|
+
}
|
|
1779
|
+
await this.log("info", "Creating session", { endpoint });
|
|
1780
|
+
const headers = { "Content-Type": "application/json" };
|
|
1781
|
+
if (this.config.sdkKey) {
|
|
1782
|
+
headers["X-SDK-Key"] = this.config.sdkKey;
|
|
1783
|
+
}
|
|
1784
|
+
const response = await fetch(endpoint, {
|
|
1785
|
+
method: "POST",
|
|
1786
|
+
headers
|
|
1787
|
+
});
|
|
1788
|
+
if (!response.ok) {
|
|
1789
|
+
await this.log("error", "Failed to create session", { status: response.status });
|
|
1790
|
+
throw new Error(`Failed to create session: ${response.status}`);
|
|
1791
|
+
}
|
|
1792
|
+
const session = await response.json();
|
|
1793
|
+
this.sessionToken = session.session_token;
|
|
1794
|
+
this.isReady = true;
|
|
1795
|
+
await this.log("info", "SDK initialized successfully", { hasSessionToken: !!this.sessionToken });
|
|
1796
|
+
if (this.config.recording?.enabled) {
|
|
1797
|
+
await this.initRecording();
|
|
1798
|
+
}
|
|
1799
|
+
if (this.config.proactive?.enabled) {
|
|
1800
|
+
this.initProactive();
|
|
1801
|
+
}
|
|
1802
|
+
if (this.config.userFlows?.enabled) {
|
|
1803
|
+
await this.initUserFlowTracking();
|
|
1804
|
+
}
|
|
1805
|
+
this.emit("ready", { sessionToken: this.sessionToken });
|
|
1806
|
+
if (this.config.initialMessage) {
|
|
1807
|
+
await this.sendInitialMessage();
|
|
1808
|
+
}
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
await this.log("error", "SDK initialization failed", { error: error instanceof Error ? error.message : String(error) });
|
|
1811
|
+
this.emit("error", error);
|
|
1812
|
+
throw error;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Register an action handler
|
|
1817
|
+
*/
|
|
1818
|
+
register(actionKey, handler) {
|
|
1819
|
+
this.actions.set(actionKey, handler);
|
|
1820
|
+
this.log("info", "Action handler registered", { actionKey, totalActions: this.actions.size });
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Unregister an action handler
|
|
1824
|
+
*/
|
|
1825
|
+
unregister(actionKey) {
|
|
1826
|
+
this.actions.delete(actionKey);
|
|
1827
|
+
this.log("info", "Action handler unregistered", { actionKey, remainingActions: this.actions.size });
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Send a message and handle potential action or flow triggers
|
|
1831
|
+
*/
|
|
1832
|
+
async sendMessage(message) {
|
|
1833
|
+
if (!this.isReady || !this.sessionToken) {
|
|
1834
|
+
throw new Error("SDK not initialized. Call init() first.");
|
|
1835
|
+
}
|
|
1836
|
+
const startTime = Date.now();
|
|
1837
|
+
await this.log("info", "sendMessage called", { message });
|
|
1838
|
+
try {
|
|
1839
|
+
let intentEndpoint;
|
|
1840
|
+
const headers = { "Content-Type": "application/json" };
|
|
1841
|
+
if (this.config.sdkKey) {
|
|
1842
|
+
intentEndpoint = `${this.config.apiUrl}/sdk/match-intent`;
|
|
1843
|
+
headers["X-SDK-Key"] = this.config.sdkKey;
|
|
1844
|
+
} else {
|
|
1845
|
+
intentEndpoint = `${this.config.apiUrl}/sdk/public/${this.config.guiderId}/match-intent`;
|
|
1846
|
+
}
|
|
1847
|
+
const intentResponse = await fetch(intentEndpoint, {
|
|
1848
|
+
method: "POST",
|
|
1849
|
+
headers,
|
|
1850
|
+
body: JSON.stringify({ userMessage: message })
|
|
1851
|
+
});
|
|
1852
|
+
await this.log("info", "Match-intent response received", { status: intentResponse.status });
|
|
1853
|
+
if (intentResponse.ok) {
|
|
1854
|
+
const intentResult = await intentResponse.json();
|
|
1855
|
+
if (intentResult.matched && intentResult.type === "flow" && intentResult.flow) {
|
|
1856
|
+
await this.log("info", "Flow matched", {
|
|
1857
|
+
flowId: intentResult.flow.flowId,
|
|
1858
|
+
name: intentResult.flow.name,
|
|
1859
|
+
stepCount: intentResult.flow.steps?.length
|
|
1860
|
+
});
|
|
1861
|
+
await this.sendLogToBackend({
|
|
1862
|
+
userMessage: message,
|
|
1863
|
+
matched: true,
|
|
1864
|
+
actionKey: `flow:${intentResult.flow.flowId}`,
|
|
1865
|
+
responseMessage: intentResult.responseMessage,
|
|
1866
|
+
executionTimeMs: Date.now() - startTime
|
|
1867
|
+
});
|
|
1868
|
+
const flowResult = await this.executeFlow(intentResult.flow);
|
|
1869
|
+
const flowMessage = {
|
|
1870
|
+
role: "assistant",
|
|
1871
|
+
content: intentResult.responseMessage || `I've completed the "${intentResult.flow.name}" flow for you.`,
|
|
1872
|
+
flow: intentResult.flow,
|
|
1873
|
+
flowResult
|
|
1874
|
+
};
|
|
1875
|
+
this.emit("message", flowMessage);
|
|
1876
|
+
return flowMessage;
|
|
1877
|
+
}
|
|
1878
|
+
if (intentResult.matched && (intentResult.type === "operation" || intentResult.action)) {
|
|
1879
|
+
const action = intentResult.action;
|
|
1880
|
+
await this.log("info", "Action matched", {
|
|
1881
|
+
actionKey: action.actionKey,
|
|
1882
|
+
actionType: action.actionType,
|
|
1883
|
+
responseMessage: action.responseMessage
|
|
1884
|
+
});
|
|
1885
|
+
await this.sendLogToBackend({
|
|
1886
|
+
userMessage: message,
|
|
1887
|
+
matched: true,
|
|
1888
|
+
actionKey: action.actionKey,
|
|
1889
|
+
responseMessage: action.responseMessage,
|
|
1890
|
+
executionTimeMs: Date.now() - startTime
|
|
1891
|
+
});
|
|
1892
|
+
await this.executeAction(action);
|
|
1893
|
+
const actionMessage = {
|
|
1894
|
+
role: "assistant",
|
|
1895
|
+
content: action.responseMessage || `I've triggered the "${action.name}" action for you.`,
|
|
1896
|
+
action
|
|
1897
|
+
};
|
|
1898
|
+
this.emit("message", actionMessage);
|
|
1899
|
+
return actionMessage;
|
|
1900
|
+
} else {
|
|
1901
|
+
await this.sendLogToBackend({
|
|
1902
|
+
userMessage: message,
|
|
1903
|
+
matched: false,
|
|
1904
|
+
executionTimeMs: Date.now() - startTime
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const sessionResponse = await fetch(
|
|
1909
|
+
`${this.config.apiUrl}/chat/sessions/${this.sessionToken}/message`,
|
|
1910
|
+
{
|
|
1911
|
+
method: "POST",
|
|
1912
|
+
headers: { "Content-Type": "application/json" },
|
|
1913
|
+
body: JSON.stringify({ message })
|
|
1914
|
+
}
|
|
1915
|
+
);
|
|
1916
|
+
if (!sessionResponse.ok) {
|
|
1917
|
+
throw new Error(`Failed to send message: ${sessionResponse.status}`);
|
|
1918
|
+
}
|
|
1919
|
+
const result = await sessionResponse.json();
|
|
1920
|
+
const chatMessage = {
|
|
1921
|
+
role: "assistant",
|
|
1922
|
+
content: result.message?.content || result.response || "",
|
|
1923
|
+
visualFlows: result.flows || [],
|
|
1924
|
+
images: result.images || []
|
|
1925
|
+
};
|
|
1926
|
+
await this.log("info", "Chat response received", {
|
|
1927
|
+
hasFlows: (result.flows || []).length > 0,
|
|
1928
|
+
hasImages: (result.images || []).length > 0
|
|
1929
|
+
});
|
|
1930
|
+
this.emit("message", chatMessage);
|
|
1931
|
+
return chatMessage;
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
await this.log("error", "Error in sendMessage", { error: error instanceof Error ? error.message : String(error) });
|
|
1934
|
+
this.emit("error", error);
|
|
1935
|
+
throw error;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Execute a flow by running each step's operation sequentially
|
|
1940
|
+
*/
|
|
1941
|
+
async executeFlow(flow) {
|
|
1942
|
+
await this.log("info", "executeFlow started", {
|
|
1943
|
+
flowId: flow.flowId,
|
|
1944
|
+
name: flow.name,
|
|
1945
|
+
stepCount: flow.steps.length
|
|
1946
|
+
});
|
|
1947
|
+
this.emit("flowStart", flow);
|
|
1948
|
+
if (this.config.onFlowStart) {
|
|
1949
|
+
this.config.onFlowStart(flow);
|
|
1950
|
+
}
|
|
1951
|
+
const stepResults = [];
|
|
1952
|
+
let context = {};
|
|
1953
|
+
const sortedSteps = [...flow.steps].sort((a, b) => a.order - b.order);
|
|
1954
|
+
for (const step of sortedSteps) {
|
|
1955
|
+
await this.log("info", "Executing flow step", {
|
|
1956
|
+
flowId: flow.flowId,
|
|
1957
|
+
operationId: step.operationId,
|
|
1958
|
+
order: step.order
|
|
1959
|
+
});
|
|
1960
|
+
const stepResult = {
|
|
1961
|
+
operationId: step.operationId,
|
|
1962
|
+
order: step.order,
|
|
1963
|
+
success: false
|
|
1964
|
+
};
|
|
1965
|
+
try {
|
|
1966
|
+
const handler = this.actions.get(step.operationId);
|
|
1967
|
+
if (handler) {
|
|
1968
|
+
const actionPayload = {
|
|
1969
|
+
actionKey: step.operationId,
|
|
1970
|
+
name: step.operationId,
|
|
1971
|
+
actionType: "callback",
|
|
1972
|
+
actionConfig: {
|
|
1973
|
+
flowContext: context,
|
|
1974
|
+
inputMapping: step.inputMapping
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
const result = await handler(actionPayload);
|
|
1978
|
+
stepResult.success = true;
|
|
1979
|
+
stepResult.data = result;
|
|
1980
|
+
context = {
|
|
1981
|
+
...context,
|
|
1982
|
+
[`step_${step.order}`]: result,
|
|
1983
|
+
previousResult: result
|
|
1984
|
+
};
|
|
1985
|
+
await this.log("info", "Flow step completed successfully", {
|
|
1986
|
+
flowId: flow.flowId,
|
|
1987
|
+
operationId: step.operationId,
|
|
1988
|
+
order: step.order
|
|
1989
|
+
});
|
|
1990
|
+
} else {
|
|
1991
|
+
await this.log("warn", "No handler registered for flow step", {
|
|
1992
|
+
flowId: flow.flowId,
|
|
1993
|
+
operationId: step.operationId
|
|
1994
|
+
});
|
|
1995
|
+
stepResult.success = true;
|
|
1996
|
+
stepResult.data = { skipped: true, reason: "No handler registered" };
|
|
1997
|
+
}
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
stepResult.success = false;
|
|
2000
|
+
stepResult.error = error instanceof Error ? error.message : String(error);
|
|
2001
|
+
await this.log("error", "Flow step failed", {
|
|
2002
|
+
flowId: flow.flowId,
|
|
2003
|
+
operationId: step.operationId,
|
|
2004
|
+
error: stepResult.error
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
stepResults.push(stepResult);
|
|
2008
|
+
this.emit("flowStepComplete", { step: stepResult, flow });
|
|
2009
|
+
if (this.config.onFlowStepComplete) {
|
|
2010
|
+
this.config.onFlowStepComplete(stepResult, flow);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
const flowResult = {
|
|
2014
|
+
flowId: flow.flowId,
|
|
2015
|
+
name: flow.name,
|
|
2016
|
+
steps: stepResults,
|
|
2017
|
+
completed: true,
|
|
2018
|
+
totalSteps: flow.steps.length,
|
|
2019
|
+
successfulSteps: stepResults.filter((s) => s.success).length
|
|
67
2020
|
};
|
|
2021
|
+
await this.log("info", "Flow execution completed", {
|
|
2022
|
+
flowId: flow.flowId,
|
|
2023
|
+
totalSteps: flowResult.totalSteps,
|
|
2024
|
+
successfulSteps: flowResult.successfulSteps
|
|
2025
|
+
});
|
|
2026
|
+
this.emit("flowComplete", flowResult);
|
|
2027
|
+
if (this.config.onFlowComplete) {
|
|
2028
|
+
this.config.onFlowComplete(flowResult);
|
|
2029
|
+
}
|
|
2030
|
+
return flowResult;
|
|
68
2031
|
}
|
|
69
2032
|
/**
|
|
70
|
-
*
|
|
2033
|
+
* Send log data to backend for analytics
|
|
71
2034
|
*/
|
|
72
|
-
async
|
|
73
|
-
await this.log("info", "SDK Initializing", {
|
|
74
|
-
apiUrl: this.config.apiUrl,
|
|
75
|
-
hasSDKKey: !!this.config.sdkKey,
|
|
76
|
-
hasGuiderId: !!this.config.guiderId
|
|
77
|
-
});
|
|
2035
|
+
async sendLogToBackend(logData) {
|
|
78
2036
|
try {
|
|
79
|
-
|
|
2037
|
+
const headers = { "Content-Type": "application/json" };
|
|
80
2038
|
if (this.config.sdkKey) {
|
|
81
|
-
|
|
82
|
-
await this.log("info", "Using SDK key authentication");
|
|
83
|
-
} else if (this.config.guiderId) {
|
|
84
|
-
endpoint = `${this.config.apiUrl}/chat/${this.config.guiderId}/session`;
|
|
85
|
-
await this.log("info", "Using guider ID authentication");
|
|
86
|
-
} else {
|
|
87
|
-
throw new Error("Either sdkKey or guiderId must be provided");
|
|
2039
|
+
headers["X-SDK-Key"] = this.config.sdkKey;
|
|
88
2040
|
}
|
|
89
|
-
|
|
2041
|
+
const logEndpoint = `${this.config.apiUrl}/sdk-logs/client`;
|
|
2042
|
+
await fetch(logEndpoint, {
|
|
2043
|
+
method: "POST",
|
|
2044
|
+
headers,
|
|
2045
|
+
body: JSON.stringify({
|
|
2046
|
+
...logData,
|
|
2047
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2048
|
+
sessionToken: this.sessionToken
|
|
2049
|
+
})
|
|
2050
|
+
});
|
|
2051
|
+
} catch (error) {
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Send detailed log to backend
|
|
2056
|
+
*/
|
|
2057
|
+
async log(level, message, data) {
|
|
2058
|
+
try {
|
|
90
2059
|
const headers = { "Content-Type": "application/json" };
|
|
91
2060
|
if (this.config.sdkKey) {
|
|
92
2061
|
headers["X-SDK-Key"] = this.config.sdkKey;
|
|
93
2062
|
}
|
|
94
|
-
const
|
|
2063
|
+
const logEndpoint = `${this.config.apiUrl}/sdk-logs/client`;
|
|
2064
|
+
await fetch(logEndpoint, {
|
|
95
2065
|
method: "POST",
|
|
96
|
-
headers
|
|
2066
|
+
headers,
|
|
2067
|
+
body: JSON.stringify({
|
|
2068
|
+
level,
|
|
2069
|
+
message,
|
|
2070
|
+
data,
|
|
2071
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2072
|
+
sessionToken: this.sessionToken
|
|
2073
|
+
})
|
|
2074
|
+
});
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Execute a registered action
|
|
2080
|
+
*/
|
|
2081
|
+
async executeAction(action) {
|
|
2082
|
+
const registeredKeys = Array.from(this.actions.keys());
|
|
2083
|
+
if (typeof window !== "undefined") {
|
|
2084
|
+
console.log("%c\u{1F3AF} SDK executeAction", "background: #ff0; color: #000; font-size: 20px; padding: 10px;", {
|
|
2085
|
+
actionKey: action.actionKey,
|
|
2086
|
+
totalRegistered: this.actions.size,
|
|
2087
|
+
registeredKeys
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
await this.log("info", "executeAction called", {
|
|
2091
|
+
actionKey: action.actionKey,
|
|
2092
|
+
actionType: typeof action.actionKey,
|
|
2093
|
+
totalRegistered: this.actions.size,
|
|
2094
|
+
registeredKeys,
|
|
2095
|
+
keyComparisons: registeredKeys.map((key) => ({
|
|
2096
|
+
key,
|
|
2097
|
+
matches: key === action.actionKey,
|
|
2098
|
+
keyLength: key.length,
|
|
2099
|
+
actionKeyLength: action.actionKey.length
|
|
2100
|
+
}))
|
|
2101
|
+
});
|
|
2102
|
+
const handler = this.actions.get(action.actionKey);
|
|
2103
|
+
if (typeof window !== "undefined") {
|
|
2104
|
+
console.log("%c\u{1F50D} SDK Handler Lookup", "background: #0ff; color: #000; font-size: 16px; padding: 5px;", {
|
|
2105
|
+
actionKey: action.actionKey,
|
|
2106
|
+
found: !!handler,
|
|
2107
|
+
hasConfigOnAction: !!this.config.onAction,
|
|
2108
|
+
registeredKeys: Array.from(this.actions.keys())
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
if (handler) {
|
|
2112
|
+
try {
|
|
2113
|
+
const startTime = Date.now();
|
|
2114
|
+
await handler(action);
|
|
2115
|
+
const executionTime = Date.now() - startTime;
|
|
2116
|
+
await this.log("info", "Handler executed successfully", {
|
|
2117
|
+
actionKey: action.actionKey,
|
|
2118
|
+
executionTime
|
|
2119
|
+
});
|
|
2120
|
+
await this.sendLogToBackend({
|
|
2121
|
+
userMessage: `Action executed: ${action.actionKey}`,
|
|
2122
|
+
matched: true,
|
|
2123
|
+
actionKey: action.actionKey,
|
|
2124
|
+
responseMessage: action.responseMessage || `Executed ${action.name}`,
|
|
2125
|
+
executionTimeMs: executionTime
|
|
2126
|
+
});
|
|
2127
|
+
this.emit("action", action);
|
|
2128
|
+
this.config.onAction?.(action.actionKey, action);
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
await this.log("error", "Handler execution failed", {
|
|
2131
|
+
actionKey: action.actionKey,
|
|
2132
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2133
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
2134
|
+
});
|
|
2135
|
+
await this.sendLogToBackend({
|
|
2136
|
+
userMessage: `Action execution failed: ${action.actionKey}`,
|
|
2137
|
+
matched: true,
|
|
2138
|
+
actionKey: action.actionKey,
|
|
2139
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2140
|
+
});
|
|
2141
|
+
this.emit("error", error);
|
|
2142
|
+
}
|
|
2143
|
+
} else {
|
|
2144
|
+
if (this.config.onAction) {
|
|
2145
|
+
await this.log("info", "No internal handler, trying config.onAction callback", {
|
|
2146
|
+
actionKey: action.actionKey
|
|
2147
|
+
});
|
|
2148
|
+
try {
|
|
2149
|
+
this.config.onAction(action.actionKey, action);
|
|
2150
|
+
this.emit("action", action);
|
|
2151
|
+
await this.log("info", "Action dispatched via config.onAction", {
|
|
2152
|
+
actionKey: action.actionKey
|
|
2153
|
+
});
|
|
2154
|
+
await this.sendLogToBackend({
|
|
2155
|
+
userMessage: `Action dispatched: ${action.actionKey}`,
|
|
2156
|
+
matched: true,
|
|
2157
|
+
actionKey: action.actionKey,
|
|
2158
|
+
responseMessage: action.responseMessage || `Dispatched ${action.name}`
|
|
2159
|
+
});
|
|
2160
|
+
return;
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
await this.log("error", "config.onAction callback failed", {
|
|
2163
|
+
actionKey: action.actionKey,
|
|
2164
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
await this.log("warn", "No handler registered for action", {
|
|
2169
|
+
actionKey: action.actionKey,
|
|
2170
|
+
registeredActions: Array.from(this.actions.keys())
|
|
2171
|
+
});
|
|
2172
|
+
await this.sendLogToBackend({
|
|
2173
|
+
userMessage: `No handler for action: ${action.actionKey}`,
|
|
2174
|
+
matched: false,
|
|
2175
|
+
actionKey: action.actionKey,
|
|
2176
|
+
error: `Handler not registered. Available: ${Array.from(this.actions.keys()).join(", ")}`
|
|
97
2177
|
});
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Add event listener
|
|
2182
|
+
*/
|
|
2183
|
+
on(event, callback) {
|
|
2184
|
+
if (!this.eventListeners.has(event)) {
|
|
2185
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
2186
|
+
}
|
|
2187
|
+
this.eventListeners.get(event).add(callback);
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Remove event listener
|
|
2191
|
+
*/
|
|
2192
|
+
off(event, callback) {
|
|
2193
|
+
this.eventListeners.get(event)?.delete(callback);
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Emit event
|
|
2197
|
+
*/
|
|
2198
|
+
emit(event, data) {
|
|
2199
|
+
this.eventListeners.get(event)?.forEach((callback) => callback(data));
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Get session token
|
|
2203
|
+
*/
|
|
2204
|
+
getSessionToken() {
|
|
2205
|
+
return this.sessionToken;
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Check if SDK is ready
|
|
2209
|
+
*/
|
|
2210
|
+
getIsReady() {
|
|
2211
|
+
return this.isReady;
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Get registered action keys
|
|
2215
|
+
*/
|
|
2216
|
+
getRegisteredActions() {
|
|
2217
|
+
return Array.from(this.actions.keys());
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Get guider ID for the current session
|
|
2221
|
+
*/
|
|
2222
|
+
getGuiderId() {
|
|
2223
|
+
return this.config.guiderId;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Fetch full visual flow details by flow ID
|
|
2227
|
+
*/
|
|
2228
|
+
async getVisualFlow(flowId) {
|
|
2229
|
+
const guiderId = this.config.guiderId;
|
|
2230
|
+
if (!guiderId) {
|
|
2231
|
+
await this.log("warn", "Cannot fetch visual flow without guiderId");
|
|
2232
|
+
return null;
|
|
2233
|
+
}
|
|
2234
|
+
try {
|
|
2235
|
+
const response = await fetch(
|
|
2236
|
+
`${this.config.apiUrl}/visual-flows/chat/${guiderId}/${flowId}`
|
|
2237
|
+
);
|
|
98
2238
|
if (!response.ok) {
|
|
99
|
-
await this.log("
|
|
100
|
-
|
|
2239
|
+
await this.log("warn", "Visual flow not found", { flowId, status: response.status });
|
|
2240
|
+
return null;
|
|
101
2241
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} catch (error) {
|
|
108
|
-
await this.log("error", "
|
|
109
|
-
|
|
110
|
-
throw error;
|
|
2242
|
+
const result = await response.json();
|
|
2243
|
+
if (!result.found || !result.flow) {
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
return result.flow;
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
await this.log("error", "Error fetching visual flow", { flowId, error });
|
|
2249
|
+
return null;
|
|
111
2250
|
}
|
|
112
2251
|
}
|
|
2252
|
+
// ============================================================
|
|
2253
|
+
// Session Recording Methods (Isolated Module)
|
|
2254
|
+
// These methods delegate to the SessionRecorder when enabled
|
|
2255
|
+
// ============================================================
|
|
113
2256
|
/**
|
|
114
|
-
*
|
|
2257
|
+
* Initialize session recording (internal)
|
|
115
2258
|
*/
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
2259
|
+
async initRecording() {
|
|
2260
|
+
try {
|
|
2261
|
+
this.recorder = new SessionRecorder(this.config.recording);
|
|
2262
|
+
this.recorder.initialize(
|
|
2263
|
+
this.sessionToken,
|
|
2264
|
+
this.config.apiUrl,
|
|
2265
|
+
this.config.sdkKey
|
|
2266
|
+
);
|
|
2267
|
+
const sessionId = await this.recorder.start();
|
|
2268
|
+
if (sessionId) {
|
|
2269
|
+
await this.log("info", "Session recording started", { recordingSessionId: sessionId });
|
|
2270
|
+
}
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
await this.log("warn", "Failed to initialize session recording", {
|
|
2273
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
119
2276
|
}
|
|
120
2277
|
/**
|
|
121
|
-
*
|
|
2278
|
+
* Start session recording manually (if not auto-started)
|
|
122
2279
|
*/
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
2280
|
+
async startRecording() {
|
|
2281
|
+
if (!this.isReady) {
|
|
2282
|
+
console.warn("[SDK] Cannot start recording before SDK is initialized");
|
|
2283
|
+
return null;
|
|
2284
|
+
}
|
|
2285
|
+
if (!this.recorder) {
|
|
2286
|
+
this.recorder = new SessionRecorder(this.config.recording);
|
|
2287
|
+
this.recorder.initialize(
|
|
2288
|
+
this.sessionToken,
|
|
2289
|
+
this.config.apiUrl,
|
|
2290
|
+
this.config.sdkKey
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
return this.recorder.start();
|
|
126
2294
|
}
|
|
127
2295
|
/**
|
|
128
|
-
*
|
|
2296
|
+
* Stop session recording
|
|
129
2297
|
*/
|
|
130
|
-
async
|
|
131
|
-
if (
|
|
132
|
-
|
|
2298
|
+
async stopRecording() {
|
|
2299
|
+
if (this.recorder) {
|
|
2300
|
+
await this.recorder.stop();
|
|
133
2301
|
}
|
|
134
|
-
|
|
135
|
-
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Pause session recording
|
|
2305
|
+
*/
|
|
2306
|
+
pauseRecording() {
|
|
2307
|
+
this.recorder?.pause();
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Resume session recording
|
|
2311
|
+
*/
|
|
2312
|
+
resumeRecording() {
|
|
2313
|
+
this.recorder?.resume();
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Add a custom event to the recording
|
|
2317
|
+
*/
|
|
2318
|
+
addRecordingEvent(event) {
|
|
2319
|
+
this.recorder?.addCustomEvent(event);
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Get recording statistics
|
|
2323
|
+
*/
|
|
2324
|
+
getRecordingStats() {
|
|
2325
|
+
return this.recorder?.getStats() ?? null;
|
|
2326
|
+
}
|
|
2327
|
+
/**
|
|
2328
|
+
* Check if session recording is active
|
|
2329
|
+
*/
|
|
2330
|
+
isRecordingActive() {
|
|
2331
|
+
return this.recorder?.isRecording() ?? false;
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Get recording session ID
|
|
2335
|
+
*/
|
|
2336
|
+
getRecordingSessionId() {
|
|
2337
|
+
return this.recorder?.getSessionId() ?? null;
|
|
2338
|
+
}
|
|
2339
|
+
// ============================================================
|
|
2340
|
+
// Proactive Chat Methods (Isolated Module)
|
|
2341
|
+
// These methods manage proactive behavior tracking and messaging
|
|
2342
|
+
// ============================================================
|
|
2343
|
+
/**
|
|
2344
|
+
* Initialize proactive behavior tracking (internal)
|
|
2345
|
+
*/
|
|
2346
|
+
initProactive() {
|
|
2347
|
+
if (!this.config.proactive) return;
|
|
2348
|
+
this.proactiveTracker = new ProactiveBehaviorTracker(
|
|
2349
|
+
this.config.proactive,
|
|
2350
|
+
{
|
|
2351
|
+
onTrigger: async (context, suggestedMessage) => {
|
|
2352
|
+
this.emit("proactive", { context, message: suggestedMessage });
|
|
2353
|
+
if (this.config.onProactiveMessage) {
|
|
2354
|
+
this.config.onProactiveMessage(suggestedMessage, context);
|
|
2355
|
+
}
|
|
2356
|
+
try {
|
|
2357
|
+
await this.getProactiveAIMessage(context, suggestedMessage);
|
|
2358
|
+
} catch (error) {
|
|
2359
|
+
await this.log("warn", "Failed to get AI proactive message", { error });
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
);
|
|
2364
|
+
this.proactiveTracker.start();
|
|
2365
|
+
this.log("info", "Proactive behavior tracking initialized");
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Get AI-generated proactive message based on context
|
|
2369
|
+
*/
|
|
2370
|
+
async getProactiveAIMessage(context, fallbackMessage) {
|
|
136
2371
|
try {
|
|
137
|
-
let intentEndpoint;
|
|
138
2372
|
const headers = { "Content-Type": "application/json" };
|
|
139
2373
|
if (this.config.sdkKey) {
|
|
140
|
-
intentEndpoint = `${this.config.apiUrl}/sdk/match-intent`;
|
|
141
2374
|
headers["X-SDK-Key"] = this.config.sdkKey;
|
|
142
|
-
} else {
|
|
143
|
-
intentEndpoint = `${this.config.apiUrl}/sdk/public/${this.config.guiderId}/match-intent`;
|
|
144
2375
|
}
|
|
145
|
-
const
|
|
2376
|
+
const response = await fetch(`${this.config.apiUrl}/sdk/proactive-message`, {
|
|
146
2377
|
method: "POST",
|
|
147
2378
|
headers,
|
|
148
|
-
body: JSON.stringify({
|
|
2379
|
+
body: JSON.stringify({
|
|
2380
|
+
context,
|
|
2381
|
+
fallbackMessage,
|
|
2382
|
+
sessionToken: this.sessionToken
|
|
2383
|
+
})
|
|
149
2384
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const intentResult = await intentResponse.json();
|
|
153
|
-
if (intentResult.matched && intentResult.type === "flow" && intentResult.flow) {
|
|
154
|
-
await this.log("info", "Flow matched", {
|
|
155
|
-
flowId: intentResult.flow.flowId,
|
|
156
|
-
name: intentResult.flow.name,
|
|
157
|
-
stepCount: intentResult.flow.steps?.length
|
|
158
|
-
});
|
|
159
|
-
await this.sendLogToBackend({
|
|
160
|
-
userMessage: message,
|
|
161
|
-
matched: true,
|
|
162
|
-
actionKey: `flow:${intentResult.flow.flowId}`,
|
|
163
|
-
responseMessage: intentResult.responseMessage,
|
|
164
|
-
executionTimeMs: Date.now() - startTime
|
|
165
|
-
});
|
|
166
|
-
const flowResult = await this.executeFlow(intentResult.flow);
|
|
167
|
-
const flowMessage = {
|
|
168
|
-
role: "assistant",
|
|
169
|
-
content: intentResult.responseMessage || `I've completed the "${intentResult.flow.name}" flow for you.`,
|
|
170
|
-
flow: intentResult.flow,
|
|
171
|
-
flowResult
|
|
172
|
-
};
|
|
173
|
-
this.emit("message", flowMessage);
|
|
174
|
-
return flowMessage;
|
|
175
|
-
}
|
|
176
|
-
if (intentResult.matched && (intentResult.type === "operation" || intentResult.action)) {
|
|
177
|
-
const action = intentResult.action;
|
|
178
|
-
await this.log("info", "Action matched", {
|
|
179
|
-
actionKey: action.actionKey,
|
|
180
|
-
actionType: action.actionType,
|
|
181
|
-
responseMessage: action.responseMessage
|
|
182
|
-
});
|
|
183
|
-
await this.sendLogToBackend({
|
|
184
|
-
userMessage: message,
|
|
185
|
-
matched: true,
|
|
186
|
-
actionKey: action.actionKey,
|
|
187
|
-
responseMessage: action.responseMessage,
|
|
188
|
-
executionTimeMs: Date.now() - startTime
|
|
189
|
-
});
|
|
190
|
-
await this.executeAction(action);
|
|
191
|
-
const actionMessage = {
|
|
192
|
-
role: "assistant",
|
|
193
|
-
content: action.responseMessage || `I've triggered the "${action.name}" action for you.`,
|
|
194
|
-
action
|
|
195
|
-
};
|
|
196
|
-
this.emit("message", actionMessage);
|
|
197
|
-
return actionMessage;
|
|
198
|
-
} else {
|
|
199
|
-
await this.sendLogToBackend({
|
|
200
|
-
userMessage: message,
|
|
201
|
-
matched: false,
|
|
202
|
-
executionTimeMs: Date.now() - startTime
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const sessionResponse = await fetch(
|
|
207
|
-
`${this.config.apiUrl}/chat/sessions/${this.sessionToken}/message`,
|
|
208
|
-
{
|
|
209
|
-
method: "POST",
|
|
210
|
-
headers: { "Content-Type": "application/json" },
|
|
211
|
-
body: JSON.stringify({ message })
|
|
212
|
-
}
|
|
213
|
-
);
|
|
214
|
-
if (!sessionResponse.ok) {
|
|
215
|
-
throw new Error(`Failed to send message: ${sessionResponse.status}`);
|
|
2385
|
+
if (!response.ok) {
|
|
2386
|
+
return fallbackMessage;
|
|
216
2387
|
}
|
|
217
|
-
const result = await
|
|
2388
|
+
const result = await response.json();
|
|
2389
|
+
const message = result.message || fallbackMessage;
|
|
218
2390
|
const chatMessage = {
|
|
219
2391
|
role: "assistant",
|
|
220
|
-
content:
|
|
2392
|
+
content: message
|
|
221
2393
|
};
|
|
222
|
-
await this.log("info", "Chat response received");
|
|
223
2394
|
this.emit("message", chatMessage);
|
|
224
|
-
return
|
|
2395
|
+
return message;
|
|
225
2396
|
} catch (error) {
|
|
226
|
-
|
|
227
|
-
this.emit("error", error);
|
|
228
|
-
throw error;
|
|
2397
|
+
return fallbackMessage;
|
|
229
2398
|
}
|
|
230
2399
|
}
|
|
231
2400
|
/**
|
|
232
|
-
*
|
|
2401
|
+
* Send initial message for landing page intros
|
|
233
2402
|
*/
|
|
234
|
-
async
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
const stepResults = [];
|
|
245
|
-
let context = {};
|
|
246
|
-
const sortedSteps = [...flow.steps].sort((a, b) => a.order - b.order);
|
|
247
|
-
for (const step of sortedSteps) {
|
|
248
|
-
await this.log("info", "Executing flow step", {
|
|
249
|
-
flowId: flow.flowId,
|
|
250
|
-
operationId: step.operationId,
|
|
251
|
-
order: step.order
|
|
252
|
-
});
|
|
253
|
-
const stepResult = {
|
|
254
|
-
operationId: step.operationId,
|
|
255
|
-
order: step.order,
|
|
256
|
-
success: false
|
|
2403
|
+
async sendInitialMessage() {
|
|
2404
|
+
const message = this.config.initialMessage;
|
|
2405
|
+
if (!message) return;
|
|
2406
|
+
if (this.config.streamInitialMessage) {
|
|
2407
|
+
await this.streamMessage(message);
|
|
2408
|
+
} else {
|
|
2409
|
+
const chatMessage = {
|
|
2410
|
+
role: "assistant",
|
|
2411
|
+
content: message
|
|
257
2412
|
};
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const actionPayload = {
|
|
262
|
-
actionKey: step.operationId,
|
|
263
|
-
name: step.operationId,
|
|
264
|
-
actionType: "callback",
|
|
265
|
-
actionConfig: {
|
|
266
|
-
flowContext: context,
|
|
267
|
-
inputMapping: step.inputMapping
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
const result = await handler(actionPayload);
|
|
271
|
-
stepResult.success = true;
|
|
272
|
-
stepResult.data = result;
|
|
273
|
-
context = {
|
|
274
|
-
...context,
|
|
275
|
-
[`step_${step.order}`]: result,
|
|
276
|
-
previousResult: result
|
|
277
|
-
};
|
|
278
|
-
await this.log("info", "Flow step completed successfully", {
|
|
279
|
-
flowId: flow.flowId,
|
|
280
|
-
operationId: step.operationId,
|
|
281
|
-
order: step.order
|
|
282
|
-
});
|
|
283
|
-
} else {
|
|
284
|
-
await this.log("warn", "No handler registered for flow step", {
|
|
285
|
-
flowId: flow.flowId,
|
|
286
|
-
operationId: step.operationId
|
|
287
|
-
});
|
|
288
|
-
stepResult.success = true;
|
|
289
|
-
stepResult.data = { skipped: true, reason: "No handler registered" };
|
|
290
|
-
}
|
|
291
|
-
} catch (error) {
|
|
292
|
-
stepResult.success = false;
|
|
293
|
-
stepResult.error = error instanceof Error ? error.message : String(error);
|
|
294
|
-
await this.log("error", "Flow step failed", {
|
|
295
|
-
flowId: flow.flowId,
|
|
296
|
-
operationId: step.operationId,
|
|
297
|
-
error: stepResult.error
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
stepResults.push(stepResult);
|
|
301
|
-
this.emit("flowStepComplete", { step: stepResult, flow });
|
|
302
|
-
if (this.config.onFlowStepComplete) {
|
|
303
|
-
this.config.onFlowStepComplete(stepResult, flow);
|
|
2413
|
+
this.emit("message", chatMessage);
|
|
2414
|
+
if (this.config.onMessage) {
|
|
2415
|
+
this.config.onMessage(chatMessage);
|
|
304
2416
|
}
|
|
305
2417
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (this.config.onFlowComplete) {
|
|
321
|
-
this.config.onFlowComplete(flowResult);
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Stream a message character by character for typewriter effect
|
|
2421
|
+
*/
|
|
2422
|
+
async streamMessage(message, delayMs = 30) {
|
|
2423
|
+
let currentContent = "";
|
|
2424
|
+
for (let i = 0; i < message.length; i++) {
|
|
2425
|
+
currentContent += message[i];
|
|
2426
|
+
const chatMessage = {
|
|
2427
|
+
role: "assistant",
|
|
2428
|
+
content: currentContent
|
|
2429
|
+
};
|
|
2430
|
+
this.emit("message", chatMessage);
|
|
2431
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
322
2432
|
}
|
|
323
|
-
return flowResult;
|
|
324
2433
|
}
|
|
325
2434
|
/**
|
|
326
|
-
*
|
|
2435
|
+
* Start proactive tracking manually
|
|
327
2436
|
*/
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
2437
|
+
startProactiveTracking() {
|
|
2438
|
+
if (this.proactiveTracker) {
|
|
2439
|
+
this.proactiveTracker.start();
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Stop proactive tracking
|
|
2444
|
+
*/
|
|
2445
|
+
stopProactiveTracking() {
|
|
2446
|
+
if (this.proactiveTracker) {
|
|
2447
|
+
this.proactiveTracker.stop();
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Get proactive tracking stats
|
|
2452
|
+
*/
|
|
2453
|
+
getProactiveStats() {
|
|
2454
|
+
return this.proactiveTracker?.getStats() ?? null;
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Manually trigger a proactive message
|
|
2458
|
+
*/
|
|
2459
|
+
triggerProactiveMessage(message, context) {
|
|
2460
|
+
if (this.proactiveTracker) {
|
|
2461
|
+
this.proactiveTracker.triggerManual(message, context);
|
|
2462
|
+
} else {
|
|
2463
|
+
const chatMessage = {
|
|
2464
|
+
role: "assistant",
|
|
2465
|
+
content: message
|
|
2466
|
+
};
|
|
2467
|
+
this.emit("message", chatMessage);
|
|
2468
|
+
if (this.config.onMessage) {
|
|
2469
|
+
this.config.onMessage(chatMessage);
|
|
2470
|
+
}
|
|
345
2471
|
}
|
|
346
2472
|
}
|
|
347
2473
|
/**
|
|
348
|
-
*
|
|
2474
|
+
* Opt out of proactive messages (respects user preference)
|
|
349
2475
|
*/
|
|
350
|
-
|
|
2476
|
+
optOutProactive() {
|
|
2477
|
+
this.proactiveTracker?.optOut();
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Opt in to proactive messages
|
|
2481
|
+
*/
|
|
2482
|
+
optInProactive() {
|
|
2483
|
+
this.proactiveTracker?.optIn();
|
|
2484
|
+
}
|
|
2485
|
+
// ============================================================
|
|
2486
|
+
// User Flow Tracking Methods (Isolated Module)
|
|
2487
|
+
// These methods manage user interaction tracking and flow analysis
|
|
2488
|
+
// ============================================================
|
|
2489
|
+
/**
|
|
2490
|
+
* Initialize user flow tracking (internal)
|
|
2491
|
+
*/
|
|
2492
|
+
async initUserFlowTracking() {
|
|
351
2493
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
2494
|
+
this.userFlowTracker = new UserFlowTracker(this.config.userFlows);
|
|
2495
|
+
this.userFlowTracker.initialize(
|
|
2496
|
+
this.sessionToken,
|
|
2497
|
+
this.config.apiUrl,
|
|
2498
|
+
this.config.sdkKey
|
|
2499
|
+
);
|
|
2500
|
+
const recordingSessionId = this.recorder?.getSessionId?.() || null;
|
|
2501
|
+
const sessionId = await this.userFlowTracker.start(recordingSessionId);
|
|
2502
|
+
if (sessionId) {
|
|
2503
|
+
await this.log("info", "User flow tracking started", {
|
|
2504
|
+
flowSessionId: sessionId,
|
|
2505
|
+
linkedRecordingSession: recordingSessionId
|
|
2506
|
+
});
|
|
355
2507
|
}
|
|
356
|
-
const logEndpoint = `${this.config.apiUrl}/sdk-logs/client`;
|
|
357
|
-
await fetch(logEndpoint, {
|
|
358
|
-
method: "POST",
|
|
359
|
-
headers,
|
|
360
|
-
body: JSON.stringify({
|
|
361
|
-
level,
|
|
362
|
-
message,
|
|
363
|
-
data,
|
|
364
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
365
|
-
sessionToken: this.sessionToken
|
|
366
|
-
})
|
|
367
|
-
});
|
|
368
2508
|
} catch (error) {
|
|
2509
|
+
await this.log("warn", "Failed to initialize user flow tracking", {
|
|
2510
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2511
|
+
});
|
|
369
2512
|
}
|
|
370
2513
|
}
|
|
371
2514
|
/**
|
|
372
|
-
*
|
|
2515
|
+
* Identify the user for flow tracking and segmentation
|
|
2516
|
+
* Call this when you know who the user is (e.g., after login)
|
|
373
2517
|
*/
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
console.log("%c\u{1F3AF} SDK executeAction", "background: #ff0; color: #000; font-size: 20px; padding: 10px;", {
|
|
378
|
-
actionKey: action.actionKey,
|
|
379
|
-
totalRegistered: this.actions.size,
|
|
380
|
-
registeredKeys
|
|
381
|
-
});
|
|
2518
|
+
identify(user) {
|
|
2519
|
+
if (this.userFlowTracker) {
|
|
2520
|
+
this.userFlowTracker.identify(user);
|
|
382
2521
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
registeredKeys,
|
|
388
|
-
keyComparisons: registeredKeys.map((key) => ({
|
|
389
|
-
key,
|
|
390
|
-
matches: key === action.actionKey,
|
|
391
|
-
keyLength: key.length,
|
|
392
|
-
actionKeyLength: action.actionKey.length
|
|
393
|
-
}))
|
|
2522
|
+
this.log("info", "User identified", {
|
|
2523
|
+
hasEmail: !!user.email,
|
|
2524
|
+
hasName: !!user.name,
|
|
2525
|
+
hasTags: !!user.tags && Object.keys(user.tags).length > 0
|
|
394
2526
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Start user flow tracking manually (if not auto-started)
|
|
2530
|
+
*/
|
|
2531
|
+
async startUserFlowTracking() {
|
|
2532
|
+
if (!this.isReady) {
|
|
2533
|
+
console.warn("[SDK] Cannot start flow tracking before SDK is initialized");
|
|
2534
|
+
return null;
|
|
403
2535
|
}
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
executionTime
|
|
412
|
-
});
|
|
413
|
-
await this.sendLogToBackend({
|
|
414
|
-
userMessage: `Action executed: ${action.actionKey}`,
|
|
415
|
-
matched: true,
|
|
416
|
-
actionKey: action.actionKey,
|
|
417
|
-
responseMessage: action.responseMessage || `Executed ${action.name}`,
|
|
418
|
-
executionTimeMs: executionTime
|
|
419
|
-
});
|
|
420
|
-
this.emit("action", action);
|
|
421
|
-
this.config.onAction?.(action.actionKey, action);
|
|
422
|
-
} catch (error) {
|
|
423
|
-
await this.log("error", "Handler execution failed", {
|
|
424
|
-
actionKey: action.actionKey,
|
|
425
|
-
error: error instanceof Error ? error.message : String(error),
|
|
426
|
-
stack: error instanceof Error ? error.stack : void 0
|
|
427
|
-
});
|
|
428
|
-
await this.sendLogToBackend({
|
|
429
|
-
userMessage: `Action execution failed: ${action.actionKey}`,
|
|
430
|
-
matched: true,
|
|
431
|
-
actionKey: action.actionKey,
|
|
432
|
-
error: error instanceof Error ? error.message : String(error)
|
|
433
|
-
});
|
|
434
|
-
this.emit("error", error);
|
|
435
|
-
}
|
|
436
|
-
} else {
|
|
437
|
-
if (this.config.onAction) {
|
|
438
|
-
await this.log("info", "No internal handler, trying config.onAction callback", {
|
|
439
|
-
actionKey: action.actionKey
|
|
440
|
-
});
|
|
441
|
-
try {
|
|
442
|
-
this.config.onAction(action.actionKey, action);
|
|
443
|
-
this.emit("action", action);
|
|
444
|
-
await this.log("info", "Action dispatched via config.onAction", {
|
|
445
|
-
actionKey: action.actionKey
|
|
446
|
-
});
|
|
447
|
-
await this.sendLogToBackend({
|
|
448
|
-
userMessage: `Action dispatched: ${action.actionKey}`,
|
|
449
|
-
matched: true,
|
|
450
|
-
actionKey: action.actionKey,
|
|
451
|
-
responseMessage: action.responseMessage || `Dispatched ${action.name}`
|
|
452
|
-
});
|
|
453
|
-
return;
|
|
454
|
-
} catch (error) {
|
|
455
|
-
await this.log("error", "config.onAction callback failed", {
|
|
456
|
-
actionKey: action.actionKey,
|
|
457
|
-
error: error instanceof Error ? error.message : String(error)
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
await this.log("warn", "No handler registered for action", {
|
|
462
|
-
actionKey: action.actionKey,
|
|
463
|
-
registeredActions: Array.from(this.actions.keys())
|
|
464
|
-
});
|
|
465
|
-
await this.sendLogToBackend({
|
|
466
|
-
userMessage: `No handler for action: ${action.actionKey}`,
|
|
467
|
-
matched: false,
|
|
468
|
-
actionKey: action.actionKey,
|
|
469
|
-
error: `Handler not registered. Available: ${Array.from(this.actions.keys()).join(", ")}`
|
|
470
|
-
});
|
|
2536
|
+
if (!this.userFlowTracker) {
|
|
2537
|
+
this.userFlowTracker = new UserFlowTracker(this.config.userFlows);
|
|
2538
|
+
this.userFlowTracker.initialize(
|
|
2539
|
+
this.sessionToken,
|
|
2540
|
+
this.config.apiUrl,
|
|
2541
|
+
this.config.sdkKey
|
|
2542
|
+
);
|
|
471
2543
|
}
|
|
2544
|
+
return this.userFlowTracker.start();
|
|
472
2545
|
}
|
|
473
2546
|
/**
|
|
474
|
-
*
|
|
2547
|
+
* Stop user flow tracking
|
|
475
2548
|
*/
|
|
476
|
-
|
|
477
|
-
if (
|
|
478
|
-
this.
|
|
2549
|
+
async stopUserFlowTracking() {
|
|
2550
|
+
if (this.userFlowTracker) {
|
|
2551
|
+
await this.userFlowTracker.stop();
|
|
479
2552
|
}
|
|
480
|
-
this.eventListeners.get(event).add(callback);
|
|
481
2553
|
}
|
|
482
2554
|
/**
|
|
483
|
-
*
|
|
2555
|
+
* Pause user flow tracking
|
|
484
2556
|
*/
|
|
485
|
-
|
|
486
|
-
this.
|
|
2557
|
+
pauseUserFlowTracking() {
|
|
2558
|
+
this.userFlowTracker?.pause();
|
|
487
2559
|
}
|
|
488
2560
|
/**
|
|
489
|
-
*
|
|
2561
|
+
* Resume user flow tracking
|
|
490
2562
|
*/
|
|
491
|
-
|
|
492
|
-
this.
|
|
2563
|
+
resumeUserFlowTracking() {
|
|
2564
|
+
this.userFlowTracker?.resume();
|
|
493
2565
|
}
|
|
494
2566
|
/**
|
|
495
|
-
*
|
|
2567
|
+
* Add a custom event to the user flow
|
|
496
2568
|
*/
|
|
497
|
-
|
|
498
|
-
|
|
2569
|
+
trackEvent(eventData) {
|
|
2570
|
+
this.userFlowTracker?.addCustomEvent(eventData);
|
|
499
2571
|
}
|
|
500
2572
|
/**
|
|
501
|
-
*
|
|
2573
|
+
* Get user flow tracking statistics
|
|
502
2574
|
*/
|
|
503
|
-
|
|
504
|
-
return this.
|
|
2575
|
+
getUserFlowStats() {
|
|
2576
|
+
return this.userFlowTracker?.getStats() ?? null;
|
|
505
2577
|
}
|
|
506
2578
|
/**
|
|
507
|
-
*
|
|
2579
|
+
* Check if user flow tracking is active
|
|
508
2580
|
*/
|
|
509
|
-
|
|
510
|
-
return
|
|
2581
|
+
isUserFlowTrackingActive() {
|
|
2582
|
+
return this.userFlowTracker?.isTracking() ?? false;
|
|
511
2583
|
}
|
|
512
2584
|
/**
|
|
513
2585
|
* Destroy the SDK instance
|
|
514
2586
|
*/
|
|
515
2587
|
destroy() {
|
|
2588
|
+
if (this.recorder) {
|
|
2589
|
+
this.recorder.destroy();
|
|
2590
|
+
this.recorder = null;
|
|
2591
|
+
}
|
|
2592
|
+
if (this.proactiveTracker) {
|
|
2593
|
+
this.proactiveTracker.stop();
|
|
2594
|
+
this.proactiveTracker = null;
|
|
2595
|
+
}
|
|
2596
|
+
if (this.userFlowTracker) {
|
|
2597
|
+
this.userFlowTracker.stop();
|
|
2598
|
+
this.userFlowTracker = null;
|
|
2599
|
+
}
|
|
516
2600
|
this.actions.clear();
|
|
517
2601
|
this.eventListeners.clear();
|
|
518
2602
|
this.sessionToken = null;
|
|
@@ -661,7 +2745,7 @@ function ProduckProvider({
|
|
|
661
2745
|
}
|
|
662
2746
|
|
|
663
2747
|
// src/react/ProduckChat.tsx
|
|
664
|
-
var
|
|
2748
|
+
var import_react5 = require("react");
|
|
665
2749
|
|
|
666
2750
|
// src/react/hooks.ts
|
|
667
2751
|
var import_react3 = require("react");
|
|
@@ -696,8 +2780,263 @@ function useProduckFlow() {
|
|
|
696
2780
|
return { activeFlow, flowResult, isExecutingFlow };
|
|
697
2781
|
}
|
|
698
2782
|
|
|
699
|
-
// src/react/
|
|
2783
|
+
// src/react/VisualFlowDisplay.tsx
|
|
2784
|
+
var import_react4 = require("react");
|
|
700
2785
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
2786
|
+
function VisualFlowDisplay({
|
|
2787
|
+
flowRef,
|
|
2788
|
+
theme = "light",
|
|
2789
|
+
primaryColor = "#f97316"
|
|
2790
|
+
}) {
|
|
2791
|
+
const { sdk } = useProduckContext();
|
|
2792
|
+
const [flow, setFlow] = (0, import_react4.useState)(null);
|
|
2793
|
+
const [loading, setLoading] = (0, import_react4.useState)(true);
|
|
2794
|
+
const [error, setError] = (0, import_react4.useState)(null);
|
|
2795
|
+
const [currentStep, setCurrentStep] = (0, import_react4.useState)(0);
|
|
2796
|
+
const [isExpanded, setIsExpanded] = (0, import_react4.useState)(false);
|
|
2797
|
+
const isDark = theme === "dark";
|
|
2798
|
+
(0, import_react4.useEffect)(() => {
|
|
2799
|
+
async function loadFlow() {
|
|
2800
|
+
if (!sdk) {
|
|
2801
|
+
setError("SDK not available");
|
|
2802
|
+
setLoading(false);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
try {
|
|
2806
|
+
const flowDetails = await sdk.getVisualFlow(flowRef.flowId);
|
|
2807
|
+
if (flowDetails) {
|
|
2808
|
+
setFlow(flowDetails);
|
|
2809
|
+
} else {
|
|
2810
|
+
setError("Flow not found");
|
|
2811
|
+
}
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
setError("Failed to load flow");
|
|
2814
|
+
} finally {
|
|
2815
|
+
setLoading(false);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
loadFlow();
|
|
2819
|
+
}, [sdk, flowRef.flowId]);
|
|
2820
|
+
const containerStyles = {
|
|
2821
|
+
marginTop: "12px",
|
|
2822
|
+
borderRadius: "12px",
|
|
2823
|
+
border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
|
|
2824
|
+
overflow: "hidden",
|
|
2825
|
+
backgroundColor: isDark ? "#1f2937" : "#ffffff"
|
|
2826
|
+
};
|
|
2827
|
+
const headerStyles = {
|
|
2828
|
+
padding: "12px 16px",
|
|
2829
|
+
backgroundColor: isDark ? "#374151" : "#f3f4f6",
|
|
2830
|
+
borderBottom: `1px solid ${isDark ? "#4b5563" : "#e5e7eb"}`,
|
|
2831
|
+
cursor: "pointer",
|
|
2832
|
+
display: "flex",
|
|
2833
|
+
alignItems: "center",
|
|
2834
|
+
justifyContent: "space-between"
|
|
2835
|
+
};
|
|
2836
|
+
const titleStyles = {
|
|
2837
|
+
display: "flex",
|
|
2838
|
+
alignItems: "center",
|
|
2839
|
+
gap: "8px",
|
|
2840
|
+
fontWeight: 600,
|
|
2841
|
+
fontSize: "14px",
|
|
2842
|
+
color: isDark ? "#f3f4f6" : "#1f2937"
|
|
2843
|
+
};
|
|
2844
|
+
const badgeStyles = {
|
|
2845
|
+
fontSize: "11px",
|
|
2846
|
+
padding: "2px 8px",
|
|
2847
|
+
borderRadius: "10px",
|
|
2848
|
+
backgroundColor: primaryColor,
|
|
2849
|
+
color: "#ffffff"
|
|
2850
|
+
};
|
|
2851
|
+
if (loading) {
|
|
2852
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: containerStyles, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...headerStyles, cursor: "default" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: titleStyles, children: [
|
|
2853
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u{1F4D6}" }),
|
|
2854
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Loading visual guide..." })
|
|
2855
|
+
] }) }) });
|
|
2856
|
+
}
|
|
2857
|
+
if (error || !flow) {
|
|
2858
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: containerStyles, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { ...headerStyles, cursor: "default" }, children: [
|
|
2859
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: titleStyles, children: [
|
|
2860
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u26A0\uFE0F" }),
|
|
2861
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: flowRef.name || flowRef.flowId })
|
|
2862
|
+
] }),
|
|
2863
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: "12px", color: isDark ? "#9ca3af" : "#6b7280" }, children: "Unable to load" })
|
|
2864
|
+
] }) });
|
|
2865
|
+
}
|
|
2866
|
+
const steps = flow.steps || [];
|
|
2867
|
+
const step = steps[currentStep];
|
|
2868
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: containerStyles, children: [
|
|
2869
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: headerStyles, onClick: () => setIsExpanded(!isExpanded), children: [
|
|
2870
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: titleStyles, children: [
|
|
2871
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u{1F4D6}" }),
|
|
2872
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: flow.name }),
|
|
2873
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: badgeStyles, children: [
|
|
2874
|
+
steps.length,
|
|
2875
|
+
" steps"
|
|
2876
|
+
] })
|
|
2877
|
+
] }),
|
|
2878
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: {
|
|
2879
|
+
fontSize: "16px",
|
|
2880
|
+
transition: "transform 0.2s",
|
|
2881
|
+
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)"
|
|
2882
|
+
}, children: "\u25BC" })
|
|
2883
|
+
] }),
|
|
2884
|
+
isExpanded && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "16px" }, children: [
|
|
2885
|
+
flow.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: {
|
|
2886
|
+
margin: "0 0 16px 0",
|
|
2887
|
+
fontSize: "13px",
|
|
2888
|
+
color: isDark ? "#9ca3af" : "#6b7280",
|
|
2889
|
+
lineHeight: 1.5
|
|
2890
|
+
}, children: flow.description }),
|
|
2891
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: {
|
|
2892
|
+
display: "flex",
|
|
2893
|
+
gap: "8px",
|
|
2894
|
+
marginBottom: "16px",
|
|
2895
|
+
overflowX: "auto",
|
|
2896
|
+
padding: "4px 0"
|
|
2897
|
+
}, children: steps.map((s, idx) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
2898
|
+
"button",
|
|
2899
|
+
{
|
|
2900
|
+
onClick: () => setCurrentStep(idx),
|
|
2901
|
+
style: {
|
|
2902
|
+
padding: "6px 12px",
|
|
2903
|
+
borderRadius: "6px",
|
|
2904
|
+
border: "none",
|
|
2905
|
+
backgroundColor: idx === currentStep ? primaryColor : isDark ? "#374151" : "#e5e7eb",
|
|
2906
|
+
color: idx === currentStep ? "#ffffff" : isDark ? "#d1d5db" : "#4b5563",
|
|
2907
|
+
fontSize: "12px",
|
|
2908
|
+
fontWeight: idx === currentStep ? 600 : 400,
|
|
2909
|
+
cursor: "pointer",
|
|
2910
|
+
whiteSpace: "nowrap",
|
|
2911
|
+
transition: "all 0.2s"
|
|
2912
|
+
},
|
|
2913
|
+
children: [
|
|
2914
|
+
idx + 1,
|
|
2915
|
+
". ",
|
|
2916
|
+
s.title || `Step ${idx + 1}`
|
|
2917
|
+
]
|
|
2918
|
+
},
|
|
2919
|
+
s.id
|
|
2920
|
+
)) }),
|
|
2921
|
+
step && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: {
|
|
2922
|
+
backgroundColor: isDark ? "#111827" : "#f9fafb",
|
|
2923
|
+
borderRadius: "8px",
|
|
2924
|
+
padding: "16px"
|
|
2925
|
+
}, children: [
|
|
2926
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("h4", { style: {
|
|
2927
|
+
margin: "0 0 8px 0",
|
|
2928
|
+
fontSize: "15px",
|
|
2929
|
+
fontWeight: 600,
|
|
2930
|
+
color: isDark ? "#f3f4f6" : "#1f2937"
|
|
2931
|
+
}, children: [
|
|
2932
|
+
"Step ",
|
|
2933
|
+
currentStep + 1,
|
|
2934
|
+
": ",
|
|
2935
|
+
step.title
|
|
2936
|
+
] }),
|
|
2937
|
+
step.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: {
|
|
2938
|
+
margin: "0 0 16px 0",
|
|
2939
|
+
fontSize: "13px",
|
|
2940
|
+
color: isDark ? "#9ca3af" : "#6b7280",
|
|
2941
|
+
lineHeight: 1.5
|
|
2942
|
+
}, children: step.description }),
|
|
2943
|
+
step.screenshotUrl && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: {
|
|
2944
|
+
borderRadius: "8px",
|
|
2945
|
+
overflow: "hidden",
|
|
2946
|
+
border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
|
|
2947
|
+
marginBottom: "12px"
|
|
2948
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2949
|
+
"img",
|
|
2950
|
+
{
|
|
2951
|
+
src: step.screenshotUrl,
|
|
2952
|
+
alt: `Step ${currentStep + 1}: ${step.title}`,
|
|
2953
|
+
style: {
|
|
2954
|
+
display: "block",
|
|
2955
|
+
width: "100%",
|
|
2956
|
+
height: "auto"
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
) }),
|
|
2960
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: {
|
|
2961
|
+
display: "flex",
|
|
2962
|
+
justifyContent: "space-between",
|
|
2963
|
+
marginTop: "12px"
|
|
2964
|
+
}, children: [
|
|
2965
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2966
|
+
"button",
|
|
2967
|
+
{
|
|
2968
|
+
onClick: () => setCurrentStep(Math.max(0, currentStep - 1)),
|
|
2969
|
+
disabled: currentStep === 0,
|
|
2970
|
+
style: {
|
|
2971
|
+
padding: "8px 16px",
|
|
2972
|
+
borderRadius: "6px",
|
|
2973
|
+
border: "none",
|
|
2974
|
+
backgroundColor: currentStep === 0 ? isDark ? "#374151" : "#e5e7eb" : primaryColor,
|
|
2975
|
+
color: currentStep === 0 ? isDark ? "#6b7280" : "#9ca3af" : "#ffffff",
|
|
2976
|
+
fontSize: "13px",
|
|
2977
|
+
cursor: currentStep === 0 ? "not-allowed" : "pointer",
|
|
2978
|
+
opacity: currentStep === 0 ? 0.5 : 1
|
|
2979
|
+
},
|
|
2980
|
+
children: "\u2190 Previous"
|
|
2981
|
+
}
|
|
2982
|
+
),
|
|
2983
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: {
|
|
2984
|
+
fontSize: "12px",
|
|
2985
|
+
color: isDark ? "#9ca3af" : "#6b7280",
|
|
2986
|
+
alignSelf: "center"
|
|
2987
|
+
}, children: [
|
|
2988
|
+
currentStep + 1,
|
|
2989
|
+
" / ",
|
|
2990
|
+
steps.length
|
|
2991
|
+
] }),
|
|
2992
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2993
|
+
"button",
|
|
2994
|
+
{
|
|
2995
|
+
onClick: () => setCurrentStep(Math.min(steps.length - 1, currentStep + 1)),
|
|
2996
|
+
disabled: currentStep === steps.length - 1,
|
|
2997
|
+
style: {
|
|
2998
|
+
padding: "8px 16px",
|
|
2999
|
+
borderRadius: "6px",
|
|
3000
|
+
border: "none",
|
|
3001
|
+
backgroundColor: currentStep === steps.length - 1 ? isDark ? "#374151" : "#e5e7eb" : primaryColor,
|
|
3002
|
+
color: currentStep === steps.length - 1 ? isDark ? "#6b7280" : "#9ca3af" : "#ffffff",
|
|
3003
|
+
fontSize: "13px",
|
|
3004
|
+
cursor: currentStep === steps.length - 1 ? "not-allowed" : "pointer",
|
|
3005
|
+
opacity: currentStep === steps.length - 1 ? 0.5 : 1
|
|
3006
|
+
},
|
|
3007
|
+
children: "Next \u2192"
|
|
3008
|
+
}
|
|
3009
|
+
)
|
|
3010
|
+
] })
|
|
3011
|
+
] }),
|
|
3012
|
+
flow.tags && flow.tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: {
|
|
3013
|
+
display: "flex",
|
|
3014
|
+
gap: "6px",
|
|
3015
|
+
marginTop: "12px",
|
|
3016
|
+
flexWrap: "wrap"
|
|
3017
|
+
}, children: flow.tags.map((tag, idx) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
3018
|
+
"span",
|
|
3019
|
+
{
|
|
3020
|
+
style: {
|
|
3021
|
+
fontSize: "11px",
|
|
3022
|
+
padding: "2px 8px",
|
|
3023
|
+
borderRadius: "4px",
|
|
3024
|
+
backgroundColor: isDark ? "#374151" : "#e5e7eb",
|
|
3025
|
+
color: isDark ? "#d1d5db" : "#4b5563"
|
|
3026
|
+
},
|
|
3027
|
+
children: [
|
|
3028
|
+
"#",
|
|
3029
|
+
tag
|
|
3030
|
+
]
|
|
3031
|
+
},
|
|
3032
|
+
idx
|
|
3033
|
+
)) })
|
|
3034
|
+
] })
|
|
3035
|
+
] });
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/react/ProduckChat.tsx
|
|
3039
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
701
3040
|
function ProduckChat({
|
|
702
3041
|
placeholder = "Ask a question...",
|
|
703
3042
|
title = "Chat Assistant",
|
|
@@ -707,21 +3046,60 @@ function ProduckChat({
|
|
|
707
3046
|
className = "",
|
|
708
3047
|
style = {},
|
|
709
3048
|
defaultOpen = false,
|
|
710
|
-
appearance = {}
|
|
3049
|
+
appearance = {},
|
|
3050
|
+
initialMessage,
|
|
3051
|
+
initialMessageDelay = 1e3,
|
|
3052
|
+
autoOpenDelay,
|
|
3053
|
+
proactiveEnabled = false,
|
|
3054
|
+
showPoweredBy = true,
|
|
3055
|
+
poweredByText = "Powered by prodact.ai",
|
|
3056
|
+
poweredByUrl = "https://prodact.ai"
|
|
711
3057
|
}) {
|
|
712
3058
|
const { messages, isLoading, sendMessage } = useProduckMessages();
|
|
713
3059
|
const isReady = useProduckReady();
|
|
714
|
-
const [input, setInput] = (0,
|
|
715
|
-
const [isOpen, setIsOpen] = (0,
|
|
716
|
-
const [isHovering, setIsHovering] = (0,
|
|
717
|
-
const messagesEndRef = (0,
|
|
718
|
-
const inputRef = (0,
|
|
719
|
-
(0,
|
|
3060
|
+
const [input, setInput] = (0, import_react5.useState)("");
|
|
3061
|
+
const [isOpen, setIsOpen] = (0, import_react5.useState)(position === "inline" ? true : defaultOpen);
|
|
3062
|
+
const [isHovering, setIsHovering] = (0, import_react5.useState)(false);
|
|
3063
|
+
const messagesEndRef = (0, import_react5.useRef)(null);
|
|
3064
|
+
const inputRef = (0, import_react5.useRef)(null);
|
|
3065
|
+
const [streamedMessage, setStreamedMessage] = (0, import_react5.useState)("");
|
|
3066
|
+
const [isStreamingInitial, setIsStreamingInitial] = (0, import_react5.useState)(false);
|
|
3067
|
+
const [hasShownInitial, setHasShownInitial] = (0, import_react5.useState)(false);
|
|
3068
|
+
(0, import_react5.useEffect)(() => {
|
|
3069
|
+
if (autoOpenDelay && autoOpenDelay > 0 && !isOpen && isReady) {
|
|
3070
|
+
const timer = setTimeout(() => {
|
|
3071
|
+
setIsOpen(true);
|
|
3072
|
+
}, autoOpenDelay);
|
|
3073
|
+
return () => clearTimeout(timer);
|
|
3074
|
+
}
|
|
3075
|
+
}, [autoOpenDelay, isReady]);
|
|
3076
|
+
(0, import_react5.useEffect)(() => {
|
|
3077
|
+
if (isOpen && initialMessage && isReady && !hasShownInitial) {
|
|
3078
|
+
const timer = setTimeout(() => {
|
|
3079
|
+
streamInitialMessage(initialMessage);
|
|
3080
|
+
}, initialMessageDelay);
|
|
3081
|
+
return () => clearTimeout(timer);
|
|
3082
|
+
}
|
|
3083
|
+
}, [isOpen, initialMessage, isReady, hasShownInitial, initialMessageDelay]);
|
|
3084
|
+
const streamInitialMessage = async (message) => {
|
|
3085
|
+
setIsStreamingInitial(true);
|
|
3086
|
+
setHasShownInitial(true);
|
|
3087
|
+
let currentContent = "";
|
|
3088
|
+
for (let i = 0; i < message.length; i++) {
|
|
3089
|
+
currentContent += message[i];
|
|
3090
|
+
setStreamedMessage(currentContent);
|
|
3091
|
+
const char = message[i];
|
|
3092
|
+
const delay = char === "\n" ? 40 : char === "." || char === "!" || char === "?" ? 25 : 20;
|
|
3093
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3094
|
+
}
|
|
3095
|
+
setIsStreamingInitial(false);
|
|
3096
|
+
};
|
|
3097
|
+
(0, import_react5.useEffect)(() => {
|
|
720
3098
|
if (messagesEndRef.current) {
|
|
721
3099
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
|
722
3100
|
}
|
|
723
3101
|
}, [messages]);
|
|
724
|
-
(0,
|
|
3102
|
+
(0, import_react5.useEffect)(() => {
|
|
725
3103
|
if (isOpen && inputRef.current) {
|
|
726
3104
|
setTimeout(() => inputRef.current?.focus(), 100);
|
|
727
3105
|
}
|
|
@@ -815,7 +3193,7 @@ function ProduckChat({
|
|
|
815
3193
|
backgroundColor: isDark ? "#111827" : "#f9fafb",
|
|
816
3194
|
flexShrink: 0
|
|
817
3195
|
};
|
|
818
|
-
const renderFloatingButton = () => /* @__PURE__ */ (0,
|
|
3196
|
+
const renderFloatingButton = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: containerStyles, className, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
819
3197
|
"button",
|
|
820
3198
|
{
|
|
821
3199
|
onClick: () => setIsOpen(true),
|
|
@@ -847,12 +3225,12 @@ function ProduckChat({
|
|
|
847
3225
|
return renderFloatingButton();
|
|
848
3226
|
}
|
|
849
3227
|
if (position === "inline" && !isReady) {
|
|
850
|
-
return /* @__PURE__ */ (0,
|
|
851
|
-
/* @__PURE__ */ (0,
|
|
852
|
-
/* @__PURE__ */ (0,
|
|
3228
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: containerStyles, className, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: chatWindowStyles, children: [
|
|
3229
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...headerStyles, justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
|
|
3230
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { animation: "spin 1s linear infinite" }, children: floatingButtonLoadingIcon }),
|
|
853
3231
|
"Loading..."
|
|
854
3232
|
] }) }),
|
|
855
|
-
/* @__PURE__ */ (0,
|
|
3233
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
|
|
856
3234
|
flex: 1,
|
|
857
3235
|
display: "flex",
|
|
858
3236
|
alignItems: "center",
|
|
@@ -861,14 +3239,14 @@ function ProduckChat({
|
|
|
861
3239
|
}, children: "Connecting to chat..." })
|
|
862
3240
|
] }) });
|
|
863
3241
|
}
|
|
864
|
-
return /* @__PURE__ */ (0,
|
|
865
|
-
/* @__PURE__ */ (0,
|
|
866
|
-
/* @__PURE__ */ (0,
|
|
867
|
-
/* @__PURE__ */ (0,
|
|
868
|
-
/* @__PURE__ */ (0,
|
|
869
|
-
/* @__PURE__ */ (0,
|
|
3242
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: containerStyles, className, children: [
|
|
3243
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: chatWindowStyles, children: [
|
|
3244
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: headerStyles, children: [
|
|
3245
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
|
|
3246
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: headerIcon }),
|
|
3247
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: title })
|
|
870
3248
|
] }),
|
|
871
|
-
position !== "inline" && showCloseButton && /* @__PURE__ */ (0,
|
|
3249
|
+
position !== "inline" && showCloseButton && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
872
3250
|
"button",
|
|
873
3251
|
{
|
|
874
3252
|
onClick: () => setIsOpen(false),
|
|
@@ -890,8 +3268,48 @@ function ProduckChat({
|
|
|
890
3268
|
}
|
|
891
3269
|
)
|
|
892
3270
|
] }),
|
|
893
|
-
/* @__PURE__ */ (0,
|
|
894
|
-
|
|
3271
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: messagesContainerStyles, children: [
|
|
3272
|
+
streamedMessage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3273
|
+
"div",
|
|
3274
|
+
{
|
|
3275
|
+
style: {
|
|
3276
|
+
display: "flex",
|
|
3277
|
+
justifyContent: "flex-start"
|
|
3278
|
+
},
|
|
3279
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
3280
|
+
"div",
|
|
3281
|
+
{
|
|
3282
|
+
style: {
|
|
3283
|
+
maxWidth: "85%",
|
|
3284
|
+
padding: "12px 16px",
|
|
3285
|
+
borderRadius: "16px 16px 16px 4px",
|
|
3286
|
+
backgroundColor: assistantMessageBackgroundColor,
|
|
3287
|
+
color: assistantMessageTextColor,
|
|
3288
|
+
fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize,
|
|
3289
|
+
lineHeight: "1.5",
|
|
3290
|
+
wordBreak: "break-word"
|
|
3291
|
+
},
|
|
3292
|
+
children: [
|
|
3293
|
+
streamedMessage,
|
|
3294
|
+
isStreamingInitial && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3295
|
+
"span",
|
|
3296
|
+
{
|
|
3297
|
+
style: {
|
|
3298
|
+
display: "inline-block",
|
|
3299
|
+
width: "2px",
|
|
3300
|
+
height: "1em",
|
|
3301
|
+
backgroundColor: "currentColor",
|
|
3302
|
+
marginLeft: "2px",
|
|
3303
|
+
animation: "blink 1s infinite"
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
)
|
|
3307
|
+
]
|
|
3308
|
+
}
|
|
3309
|
+
)
|
|
3310
|
+
}
|
|
3311
|
+
),
|
|
3312
|
+
messages.length === 0 && !streamedMessage && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
895
3313
|
"div",
|
|
896
3314
|
{
|
|
897
3315
|
style: {
|
|
@@ -900,20 +3318,20 @@ function ProduckChat({
|
|
|
900
3318
|
padding: "40px 20px"
|
|
901
3319
|
},
|
|
902
3320
|
children: [
|
|
903
|
-
/* @__PURE__ */ (0,
|
|
904
|
-
/* @__PURE__ */ (0,
|
|
905
|
-
/* @__PURE__ */ (0,
|
|
3321
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "40px", marginBottom: "16px" }, children: emptyStateIcon }),
|
|
3322
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: typeof fontSize === "number" ? `${fontSize + 2}px` : fontSize, fontWeight: 500 }, children: emptyStateTitle }),
|
|
3323
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize, marginTop: "8px", opacity: 0.8 }, children: emptyStateSubtitle })
|
|
906
3324
|
]
|
|
907
3325
|
}
|
|
908
3326
|
),
|
|
909
|
-
messages.map((msg, idx) => /* @__PURE__ */ (0,
|
|
3327
|
+
messages.map((msg, idx) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
910
3328
|
"div",
|
|
911
3329
|
{
|
|
912
3330
|
style: {
|
|
913
3331
|
display: "flex",
|
|
914
3332
|
justifyContent: msg.role === "user" ? "flex-end" : "flex-start"
|
|
915
3333
|
},
|
|
916
|
-
children: /* @__PURE__ */ (0,
|
|
3334
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
917
3335
|
"div",
|
|
918
3336
|
{
|
|
919
3337
|
style: {
|
|
@@ -928,7 +3346,7 @@ function ProduckChat({
|
|
|
928
3346
|
},
|
|
929
3347
|
children: [
|
|
930
3348
|
msg.content,
|
|
931
|
-
msg.action && /* @__PURE__ */ (0,
|
|
3349
|
+
msg.action && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
932
3350
|
"div",
|
|
933
3351
|
{
|
|
934
3352
|
style: {
|
|
@@ -942,21 +3360,68 @@ function ProduckChat({
|
|
|
942
3360
|
gap: "6px"
|
|
943
3361
|
},
|
|
944
3362
|
children: [
|
|
945
|
-
/* @__PURE__ */ (0,
|
|
946
|
-
/* @__PURE__ */ (0,
|
|
3363
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "\u26A1" }),
|
|
3364
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
947
3365
|
"Action triggered: ",
|
|
948
3366
|
msg.action.name
|
|
949
3367
|
] })
|
|
950
3368
|
]
|
|
951
3369
|
}
|
|
952
|
-
)
|
|
3370
|
+
),
|
|
3371
|
+
msg.visualFlows && msg.visualFlows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginTop: "12px" }, children: msg.visualFlows.map((flowRef, flowIdx) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3372
|
+
VisualFlowDisplay,
|
|
3373
|
+
{
|
|
3374
|
+
flowRef,
|
|
3375
|
+
theme,
|
|
3376
|
+
primaryColor
|
|
3377
|
+
},
|
|
3378
|
+
flowRef.flowId || flowIdx
|
|
3379
|
+
)) }),
|
|
3380
|
+
msg.images && msg.images.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginTop: "12px" }, children: msg.images.map((img, imgIdx) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
3381
|
+
"div",
|
|
3382
|
+
{
|
|
3383
|
+
style: {
|
|
3384
|
+
marginTop: imgIdx > 0 ? "8px" : 0,
|
|
3385
|
+
borderRadius: "8px",
|
|
3386
|
+
overflow: "hidden",
|
|
3387
|
+
border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`
|
|
3388
|
+
},
|
|
3389
|
+
children: [
|
|
3390
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3391
|
+
"img",
|
|
3392
|
+
{
|
|
3393
|
+
src: img.url,
|
|
3394
|
+
alt: img.name || "Image",
|
|
3395
|
+
style: {
|
|
3396
|
+
display: "block",
|
|
3397
|
+
maxWidth: "100%",
|
|
3398
|
+
height: "auto"
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
),
|
|
3402
|
+
img.name && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3403
|
+
"div",
|
|
3404
|
+
{
|
|
3405
|
+
style: {
|
|
3406
|
+
padding: "8px 12px",
|
|
3407
|
+
backgroundColor: isDark ? "#374151" : "#f3f4f6",
|
|
3408
|
+
fontSize: "12px",
|
|
3409
|
+
color: isDark ? "#d1d5db" : "#4b5563"
|
|
3410
|
+
},
|
|
3411
|
+
children: img.name
|
|
3412
|
+
}
|
|
3413
|
+
)
|
|
3414
|
+
]
|
|
3415
|
+
},
|
|
3416
|
+
imgIdx
|
|
3417
|
+
)) })
|
|
953
3418
|
]
|
|
954
3419
|
}
|
|
955
3420
|
)
|
|
956
3421
|
},
|
|
957
3422
|
idx
|
|
958
3423
|
)),
|
|
959
|
-
isLoading && /* @__PURE__ */ (0,
|
|
3424
|
+
isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", justifyContent: "flex-start" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
960
3425
|
"div",
|
|
961
3426
|
{
|
|
962
3427
|
style: {
|
|
@@ -966,16 +3431,16 @@ function ProduckChat({
|
|
|
966
3431
|
color: isDark ? "#9ca3af" : "#6b7280",
|
|
967
3432
|
fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize
|
|
968
3433
|
},
|
|
969
|
-
children: /* @__PURE__ */ (0,
|
|
3434
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: {
|
|
970
3435
|
display: "inline-block",
|
|
971
3436
|
animation: "pulse 1.5s ease-in-out infinite"
|
|
972
3437
|
}, children: "Thinking..." })
|
|
973
3438
|
}
|
|
974
3439
|
) }),
|
|
975
|
-
/* @__PURE__ */ (0,
|
|
3440
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: messagesEndRef })
|
|
976
3441
|
] }),
|
|
977
|
-
/* @__PURE__ */ (0,
|
|
978
|
-
/* @__PURE__ */ (0,
|
|
3442
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("form", { onSubmit: handleSubmit, style: inputContainerStyles, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "10px", alignItems: "center" }, children: [
|
|
3443
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
979
3444
|
"input",
|
|
980
3445
|
{
|
|
981
3446
|
ref: inputRef,
|
|
@@ -1006,7 +3471,7 @@ function ProduckChat({
|
|
|
1006
3471
|
}
|
|
1007
3472
|
}
|
|
1008
3473
|
),
|
|
1009
|
-
/* @__PURE__ */ (0,
|
|
3474
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1010
3475
|
"button",
|
|
1011
3476
|
{
|
|
1012
3477
|
type: "submit",
|
|
@@ -1038,14 +3503,45 @@ function ProduckChat({
|
|
|
1038
3503
|
e.currentTarget.style.boxShadow = "none";
|
|
1039
3504
|
},
|
|
1040
3505
|
children: [
|
|
1041
|
-
sendButtonIcon && /* @__PURE__ */ (0,
|
|
3506
|
+
sendButtonIcon && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: sendButtonIcon }),
|
|
1042
3507
|
sendButtonText
|
|
1043
3508
|
]
|
|
1044
3509
|
}
|
|
1045
3510
|
)
|
|
1046
|
-
] }) })
|
|
3511
|
+
] }) }),
|
|
3512
|
+
showPoweredBy && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3513
|
+
"div",
|
|
3514
|
+
{
|
|
3515
|
+
style: {
|
|
3516
|
+
padding: "8px 16px",
|
|
3517
|
+
textAlign: "center",
|
|
3518
|
+
borderTop: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`
|
|
3519
|
+
},
|
|
3520
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3521
|
+
"a",
|
|
3522
|
+
{
|
|
3523
|
+
href: poweredByUrl,
|
|
3524
|
+
target: "_blank",
|
|
3525
|
+
rel: "noopener noreferrer",
|
|
3526
|
+
style: {
|
|
3527
|
+
fontSize: "11px",
|
|
3528
|
+
color: isDark ? "#6b7280" : "#9ca3af",
|
|
3529
|
+
textDecoration: "none",
|
|
3530
|
+
transition: "color 0.2s"
|
|
3531
|
+
},
|
|
3532
|
+
onMouseEnter: (e) => {
|
|
3533
|
+
e.currentTarget.style.color = primaryColor;
|
|
3534
|
+
},
|
|
3535
|
+
onMouseLeave: (e) => {
|
|
3536
|
+
e.currentTarget.style.color = isDark ? "#6b7280" : "#9ca3af";
|
|
3537
|
+
},
|
|
3538
|
+
children: poweredByText
|
|
3539
|
+
}
|
|
3540
|
+
)
|
|
3541
|
+
}
|
|
3542
|
+
)
|
|
1047
3543
|
] }),
|
|
1048
|
-
/* @__PURE__ */ (0,
|
|
3544
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `
|
|
1049
3545
|
@keyframes pulse {
|
|
1050
3546
|
0%, 100% { opacity: 1; }
|
|
1051
3547
|
50% { opacity: 0.5; }
|
|
@@ -1059,8 +3555,8 @@ function ProduckChat({
|
|
|
1059
3555
|
}
|
|
1060
3556
|
|
|
1061
3557
|
// src/react/ProduckTarget.tsx
|
|
1062
|
-
var
|
|
1063
|
-
var
|
|
3558
|
+
var import_react6 = __toESM(require("react"));
|
|
3559
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1064
3560
|
function ProduckTarget({
|
|
1065
3561
|
actionKey,
|
|
1066
3562
|
children,
|
|
@@ -1074,8 +3570,8 @@ function ProduckTarget({
|
|
|
1074
3570
|
highlightDuration = 3e3,
|
|
1075
3571
|
scrollIntoView = true
|
|
1076
3572
|
}) {
|
|
1077
|
-
const ref = (0,
|
|
1078
|
-
const [isHighlighted, setIsHighlighted] =
|
|
3573
|
+
const ref = (0, import_react6.useRef)(null);
|
|
3574
|
+
const [isHighlighted, setIsHighlighted] = import_react6.default.useState(false);
|
|
1079
3575
|
useProduckAction(
|
|
1080
3576
|
actionKey,
|
|
1081
3577
|
(payload) => {
|
|
@@ -1091,7 +3587,7 @@ function ProduckTarget({
|
|
|
1091
3587
|
},
|
|
1092
3588
|
[scrollIntoView, highlightDuration, onTrigger]
|
|
1093
3589
|
);
|
|
1094
|
-
return /* @__PURE__ */ (0,
|
|
3590
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1095
3591
|
"div",
|
|
1096
3592
|
{
|
|
1097
3593
|
ref,
|
|
@@ -1107,6 +3603,7 @@ function ProduckTarget({
|
|
|
1107
3603
|
ProduckContext,
|
|
1108
3604
|
ProduckProvider,
|
|
1109
3605
|
ProduckTarget,
|
|
3606
|
+
VisualFlowDisplay,
|
|
1110
3607
|
useProduck,
|
|
1111
3608
|
useProduckAction,
|
|
1112
3609
|
useProduckFlow,
|