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