@prodact.ai/sdk 0.0.2 → 0.0.6

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