@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/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/core.ts
7
- var ProduckSDK = class {
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.actions = /* @__PURE__ */ new Map();
10
- this.eventListeners = /* @__PURE__ */ new Map();
11
- this.sessionToken = null;
12
- this.isReady = false;
13
- let apiUrl = config.apiUrl;
14
- if (!apiUrl) {
15
- if (typeof window !== "undefined") {
16
- 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`;
17
- } else {
18
- apiUrl = "http://localhost:4001/api/v1";
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
- * Initialize the SDK and create a chat session
210
+ * Stop recording
28
211
  */
29
- async init() {
30
- await this.log("info", "SDK Initializing", {
31
- apiUrl: this.config.apiUrl,
32
- hasSDKKey: !!this.config.sdkKey,
33
- hasGuiderId: !!this.config.guiderId
34
- });
212
+ async stop() {
213
+ if (this.state !== "recording" && this.state !== "paused") {
214
+ return;
215
+ }
35
216
  try {
36
- let endpoint;
37
- if (this.config.sdkKey) {
38
- endpoint = `${this.config.apiUrl}/sdk/session`;
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
- await this.log("info", "Creating session", { endpoint });
47
- const headers = { "Content-Type": "application/json" };
48
- if (this.config.sdkKey) {
49
- headers["X-SDK-Key"] = this.config.sdkKey;
221
+ if (this.consoleCleanup) {
222
+ this.consoleCleanup();
223
+ this.consoleCleanup = null;
50
224
  }
51
- const response = await fetch(endpoint, {
52
- method: "POST",
53
- headers
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
- const session = await response.json();
60
- this.sessionToken = session.session_token;
61
- this.isReady = true;
62
- await this.log("info", "SDK initialized successfully", { hasSessionToken: !!this.sessionToken });
63
- this.emit("ready", { sessionToken: this.sessionToken });
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
- await this.log("error", "SDK initialization failed", { error: error instanceof Error ? error.message : String(error) });
66
- this.emit("error", error);
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
- * Register an action handler
252
+ * Pause recording
72
253
  */
73
- register(actionKey, handler) {
74
- this.actions.set(actionKey, handler);
75
- this.log("info", "Action handler registered", { actionKey, totalActions: this.actions.size });
254
+ pause() {
255
+ if (this.state !== "recording") return;
256
+ this.state = "paused";
257
+ console.log("[Recording] Paused");
76
258
  }
77
259
  /**
78
- * Unregister an action handler
260
+ * Resume recording
79
261
  */
80
- unregister(actionKey) {
81
- this.actions.delete(actionKey);
82
- this.log("info", "Action handler unregistered", { actionKey, remainingActions: this.actions.size });
262
+ resume() {
263
+ if (this.state !== "paused") return;
264
+ this.state = "recording";
265
+ console.log("[Recording] Resumed");
83
266
  }
84
267
  /**
85
- * Send a message and handle potential action or flow triggers
268
+ * Add a custom event to the recording
86
269
  */
87
- async sendMessage(message) {
88
- if (!this.isReady || !this.sessionToken) {
89
- throw new Error("SDK not initialized. Call init() first.");
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
- const startTime = Date.now();
92
- await this.log("info", "sendMessage called", { message });
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
- let intentEndpoint;
349
+ const endpoint = this.config.endpoint || `${this.apiUrl}/session-recordings/events`;
95
350
  const headers = { "Content-Type": "application/json" };
96
- if (this.config.sdkKey) {
97
- intentEndpoint = `${this.config.apiUrl}/sdk/match-intent`;
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 intentResponse = await fetch(intentEndpoint, {
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: JSON.stringify({ userMessage: message })
358
+ body
106
359
  });
107
- await this.log("info", "Match-intent response received", { status: intentResponse.status });
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
- await this.log("error", "Error in sendMessage", { error: error instanceof Error ? error.message : String(error) });
184
- this.emit("error", error);
185
- throw error;
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
- * Execute a flow by running each step's operation sequentially
368
+ * Send session start notification
190
369
  */
191
- async executeFlow(flow) {
192
- await this.log("info", "executeFlow started", {
193
- flowId: flow.flowId,
194
- name: flow.name,
195
- stepCount: flow.steps.length
196
- });
197
- this.emit("flowStart", flow);
198
- if (this.config.onFlowStart) {
199
- this.config.onFlowStart(flow);
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
- const stepResults = [];
202
- let context = {};
203
- const sortedSteps = [...flow.steps].sort((a, b) => a.order - b.order);
204
- for (const step of sortedSteps) {
205
- await this.log("info", "Executing flow step", {
206
- flowId: flow.flowId,
207
- operationId: step.operationId,
208
- order: step.order
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
- const stepResult = {
211
- operationId: step.operationId,
212
- order: step.order,
213
- success: false
214
- };
215
- try {
216
- const handler = this.actions.get(step.operationId);
217
- if (handler) {
218
- const actionPayload = {
219
- actionKey: step.operationId,
220
- name: step.operationId,
221
- actionType: "callback",
222
- actionConfig: {
223
- flowContext: context,
224
- inputMapping: step.inputMapping
225
- }
226
- };
227
- const result = await handler(actionPayload);
228
- stepResult.success = true;
229
- stepResult.data = result;
230
- context = {
231
- ...context,
232
- [`step_${step.order}`]: result,
233
- previousResult: result
234
- };
235
- await this.log("info", "Flow step completed successfully", {
236
- flowId: flow.flowId,
237
- operationId: step.operationId,
238
- order: step.order
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
- } else {
241
- await this.log("warn", "No handler registered for flow step", {
242
- flowId: flow.flowId,
243
- operationId: step.operationId
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
- } catch (error) {
249
- stepResult.success = false;
250
- stepResult.error = error instanceof Error ? error.message : String(error);
251
- await this.log("error", "Flow step failed", {
252
- flowId: flow.flowId,
253
- operationId: step.operationId,
254
- error: stepResult.error
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
- stepResults.push(stepResult);
258
- this.emit("flowStepComplete", { step: stepResult, flow });
259
- if (this.config.onFlowStepComplete) {
260
- this.config.onFlowStepComplete(stepResult, flow);
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
- const flowResult = {
264
- flowId: flow.flowId,
265
- name: flow.name,
266
- steps: stepResults,
267
- completed: true,
268
- totalSteps: flow.steps.length,
269
- successfulSteps: stepResults.filter((s) => s.success).length
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
- await this.log("info", "Flow execution completed", {
272
- flowId: flow.flowId,
273
- totalSteps: flowResult.totalSteps,
274
- successfulSteps: flowResult.successfulSteps
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
- this.emit("flowComplete", flowResult);
277
- if (this.config.onFlowComplete) {
278
- this.config.onFlowComplete(flowResult);
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
- return flowResult;
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
- * Send log data to backend for analytics
2436
+ * Opt in to proactive messages
284
2437
  */
285
- async sendLogToBackend(logData) {
286
- try {
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
- * Send detailed log to backend
2446
+ * Initialize user flow tracking (internal)
306
2447
  */
307
- async log(level, message, data) {
2448
+ async initUserFlowTracking() {
308
2449
  try {
309
- const headers = { "Content-Type": "application/json" };
310
- if (this.config.sdkKey) {
311
- headers["X-SDK-Key"] = this.config.sdkKey;
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
- * Execute a registered action
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
- async executeAction(action) {
332
- const registeredKeys = Array.from(this.actions.keys());
333
- if (typeof window !== "undefined") {
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
- await this.log("info", "executeAction called", {
341
- actionKey: action.actionKey,
342
- actionType: typeof action.actionKey,
343
- totalRegistered: this.actions.size,
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
- const handler = this.actions.get(action.actionKey);
353
- if (typeof window !== "undefined") {
354
- console.log("%c\u{1F50D} SDK Handler Lookup", "background: #0ff; color: #000; font-size: 16px; padding: 5px;", {
355
- actionKey: action.actionKey,
356
- found: !!handler,
357
- hasConfigOnAction: !!this.config.onAction,
358
- registeredKeys: Array.from(this.actions.keys())
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 (handler) {
362
- try {
363
- const startTime = Date.now();
364
- await handler(action);
365
- const executionTime = Date.now() - startTime;
366
- await this.log("info", "Handler executed successfully", {
367
- actionKey: action.actionKey,
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
- * Add event listener
2503
+ * Stop user flow tracking
432
2504
  */
433
- on(event, callback) {
434
- if (!this.eventListeners.has(event)) {
435
- this.eventListeners.set(event, /* @__PURE__ */ new Set());
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
- * Remove event listener
2511
+ * Pause user flow tracking
441
2512
  */
442
- off(event, callback) {
443
- this.eventListeners.get(event)?.delete(callback);
2513
+ pauseUserFlowTracking() {
2514
+ this.userFlowTracker?.pause();
444
2515
  }
445
2516
  /**
446
- * Emit event
2517
+ * Resume user flow tracking
447
2518
  */
448
- emit(event, data) {
449
- this.eventListeners.get(event)?.forEach((callback) => callback(data));
2519
+ resumeUserFlowTracking() {
2520
+ this.userFlowTracker?.resume();
450
2521
  }
451
2522
  /**
452
- * Get session token
2523
+ * Add a custom event to the user flow
453
2524
  */
454
- getSessionToken() {
455
- return this.sessionToken;
2525
+ trackEvent(eventData) {
2526
+ this.userFlowTracker?.addCustomEvent(eventData);
456
2527
  }
457
2528
  /**
458
- * Check if SDK is ready
2529
+ * Get user flow tracking statistics
459
2530
  */
460
- getIsReady() {
461
- return this.isReady;
2531
+ getUserFlowStats() {
2532
+ return this.userFlowTracker?.getStats() ?? null;
462
2533
  }
463
2534
  /**
464
- * Get registered action keys
2535
+ * Check if user flow tracking is active
465
2536
  */
466
- getRegisteredActions() {
467
- return Array.from(this.actions.keys());
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 useState2, useRef as useRef2, useEffect as useEffect3 } from "react";
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/ProduckChat.tsx
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] = useState2("");
672
- const [isOpen, setIsOpen] = useState2(position === "inline" ? true : defaultOpen);
673
- const [isHovering, setIsHovering] = useState2(false);
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
- useEffect3(() => {
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
- useEffect3(() => {
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__ */ jsx2("div", { style: containerStyles, className, children: /* @__PURE__ */ jsx2(
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__ */ jsx2("div", { style: containerStyles, className, children: /* @__PURE__ */ jsxs("div", { style: chatWindowStyles, children: [
808
- /* @__PURE__ */ jsx2("div", { style: { ...headerStyles, justifyContent: "center" }, children: /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
809
- /* @__PURE__ */ jsx2("span", { style: { animation: "spin 1s linear infinite" }, children: floatingButtonLoadingIcon }),
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__ */ jsx2("div", { style: {
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__ */ jsxs("div", { style: containerStyles, className, children: [
822
- /* @__PURE__ */ jsxs("div", { style: chatWindowStyles, children: [
823
- /* @__PURE__ */ jsxs("div", { style: headerStyles, children: [
824
- /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
825
- /* @__PURE__ */ jsx2("span", { children: headerIcon }),
826
- /* @__PURE__ */ jsx2("span", { children: title })
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__ */ jsx2(
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__ */ jsxs("div", { style: messagesContainerStyles, children: [
851
- messages.length === 0 && /* @__PURE__ */ jsxs(
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__ */ jsx2("div", { style: { fontSize: "40px", marginBottom: "16px" }, children: emptyStateIcon }),
861
- /* @__PURE__ */ jsx2("div", { style: { fontSize: typeof fontSize === "number" ? `${fontSize + 2}px` : fontSize, fontWeight: 500 }, children: emptyStateTitle }),
862
- /* @__PURE__ */ jsx2("div", { style: { fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize, marginTop: "8px", opacity: 0.8 }, children: emptyStateSubtitle })
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__ */ jsx2(
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__ */ jsxs(
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__ */ jsxs(
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__ */ jsx2("span", { children: "\u26A1" }),
903
- /* @__PURE__ */ jsxs("span", { children: [
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__ */ jsx2("div", { style: { display: "flex", justifyContent: "flex-start" }, children: /* @__PURE__ */ jsx2(
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__ */ jsx2("span", { style: {
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__ */ jsx2("div", { ref: messagesEndRef })
3396
+ /* @__PURE__ */ jsx3("div", { ref: messagesEndRef })
933
3397
  ] }),
934
- /* @__PURE__ */ jsx2("form", { onSubmit: handleSubmit, style: inputContainerStyles, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "10px", alignItems: "center" }, children: [
935
- /* @__PURE__ */ jsx2(
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__ */ jsxs(
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__ */ jsx2("span", { children: sendButtonIcon }),
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__ */ jsx2("style", { children: `
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 React3, { useRef as useRef3 } from "react";
1020
- import { jsx as jsx3 } from "react/jsx-runtime";
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] = React3.useState(false);
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__ */ jsx3(
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,