@@ -4,10 +4,10 @@ const styleId = "copilot-tour-styles";
4
4
const copilotInfoContainerId = "copilot-info-container" ;
5
5
6
6
function estimateReadTime ( text ) {
7
- const wordsPerSecond = 3.5 ; // Average reading speed
7
+ const wordsPerSecond = 3.6 ; // Average reading speed
8
8
const wordCount = text . split ( / \s + / ) . filter ( ( word ) => word . length > 0 ) . length ;
9
9
const readTimeInSeconds = Math . ceil ( wordCount / wordsPerSecond ) ;
10
- return readTimeInSeconds + 1.5 ;
10
+ return readTimeInSeconds + 1 ;
11
11
}
12
12
13
13
function htmlToPlainText ( html ) {
@@ -26,6 +26,26 @@ function scrollToElement(element) {
26
26
}
27
27
}
28
28
29
+ function performClickAnimation ( posX , posY ) {
30
+ // Create a new div element to act as the wave
31
+ const wave = document . createElement ( "div" ) ;
32
+
33
+ // Apply the CSS class for styling
34
+ wave . className = "click-wave" ;
35
+
36
+ // Set the position dynamically
37
+ wave . style . left = `${ posX - 17 } px` ;
38
+ wave . style . top = `${ posY - 17 } px` ;
39
+
40
+ // Append the wave to the body
41
+ document . body . appendChild ( wave ) ;
42
+
43
+ // Remove the wave after the animation ends
44
+ setTimeout ( ( ) => {
45
+ wave . remove ( ) ;
46
+ } , 800 ) ;
47
+ }
48
+
29
49
function waitForElement ( selector , timeout = 5000 ) {
30
50
const pollInterval = 100 ;
31
51
const maxAttempts = timeout / pollInterval ;
@@ -62,6 +82,7 @@ export default class GleapCopilotTours {
62
82
currentActiveIndex = undefined ;
63
83
lastArrowPositionX = undefined ;
64
84
lastArrowPositionY = undefined ;
85
+ onCompleteCallback = undefined ;
65
86
66
87
// GleapReplayRecorder singleton
67
88
static instance ;
@@ -102,7 +123,7 @@ export default class GleapCopilotTours {
102
123
} ) ;
103
124
}
104
125
105
- startWithConfig ( tourId , config , delay = 0 ) {
126
+ startWithConfig ( tourId , config , onCompleteCallback = undefined ) {
106
127
// Prevent multiple tours from being started.
107
128
if ( this . productTourId ) {
108
129
return ;
@@ -111,27 +132,8 @@ export default class GleapCopilotTours {
111
132
this . productTourId = tourId ;
112
133
this . productTourData = config ;
113
134
this . currentActiveIndex = 0 ;
114
-
115
- const self = this ;
116
-
117
- if ( delay > 0 ) {
118
- return setTimeout ( ( ) => {
119
- self . start ( ) ;
120
- } , delay ) ;
121
- } else {
122
- return this . start ( ) ;
123
- }
124
- }
125
-
126
- loadUncompletedTour ( ) {
127
- try {
128
- const data = JSON . parse ( localStorage . getItem ( localStorageKey ) ) ;
129
- if ( data ?. tourData && data ?. tourId ) {
130
- return data ;
131
- }
132
- } catch ( e ) { }
133
-
134
- return null ;
135
+ this . onCompleteCallback = onCompleteCallback ;
136
+ this . start ( ) ;
135
137
}
136
138
137
139
storeUncompletedTour ( ) {
@@ -154,14 +156,8 @@ export default class GleapCopilotTours {
154
156
localStorage . setItem ( localStorageKey , JSON . stringify ( data ) ) ;
155
157
} catch ( e ) { }
156
158
} else {
157
- this . clearUncompletedTour ( ) ;
158
- }
159
- }
160
-
161
- clearUncompletedTour ( ) {
162
- try {
163
159
localStorage . removeItem ( localStorageKey ) ;
164
- } catch ( e ) { }
160
+ }
165
161
}
166
162
167
163
updatePointerPosition ( anchor ) {
@@ -199,7 +195,7 @@ export default class GleapCopilotTours {
199
195
let anchorCenterY =
200
196
anchorRect . top + anchorRect . height / 2 + window . scrollY ;
201
197
202
- let containerWidthSpace = 330 ;
198
+ let containerWidthSpace = 350 ;
203
199
if ( containerWidthSpace > window . innerWidth - 40 ) {
204
200
containerWidthSpace = window . innerWidth - 40 ;
205
201
}
@@ -230,29 +226,24 @@ export default class GleapCopilotTours {
230
226
}
231
227
232
228
cleanup ( ) {
233
- this . removePointerUI ( ) ;
234
- this . clearUncompletedTour ( ) ;
235
- }
236
-
237
- removePointerUI ( ) {
238
229
const container = document . getElementById ( pointerContainerId ) ;
239
230
if ( container ) {
240
231
container . remove ( ) ;
241
232
}
242
233
243
- // Remove style node.
244
- const styleNode = document . getElementById ( styleId ) ;
245
- if ( styleNode ) {
246
- styleNode . remove ( ) ;
247
- }
248
-
249
- // Remove copilot info container.
250
234
const copilotInfoContainer = document . getElementById (
251
235
copilotInfoContainerId
252
236
) ;
253
237
if ( copilotInfoContainer ) {
254
238
copilotInfoContainer . remove ( ) ;
255
239
}
240
+
241
+ setTimeout ( ( ) => {
242
+ const styleNode = document . getElementById ( styleId ) ;
243
+ if ( styleNode ) {
244
+ styleNode . remove ( ) ;
245
+ }
246
+ } , 1000 ) ;
256
247
}
257
248
258
249
setupCopilotTour ( ) {
@@ -279,7 +270,7 @@ export default class GleapCopilotTours {
279
270
height: auto;
280
271
fill: none;
281
272
}
282
-
273
+
283
274
.${ pointerContainerId } -right {
284
275
left: auto;
285
276
right: 0;
@@ -307,7 +298,57 @@ export default class GleapCopilotTours {
307
298
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
308
299
}
309
300
310
- body::before {
301
+ .copilot-info-container {
302
+ position: fixed;
303
+ top: 20px;
304
+ right: 20px;
305
+ z-index: 2147483612;
306
+ background: #fff;
307
+ padding: 5px;
308
+ padding-left: 10px;
309
+ border-radius: 10px;
310
+ box-shadow: 0 0 20px 0 #e721b263;
311
+ font-family: sans-serif;
312
+ font-size: 13px;
313
+ color: #000;
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 10px;
317
+ border: 1px solid #e721b3;
318
+ max-width: min(330px, 100vw - 40px);
319
+ }
320
+
321
+ .copilot-info-container svg {
322
+ width: 24px;
323
+ height: 24px;
324
+ flex-shrink: 0;
325
+ }
326
+
327
+ .click-wave {
328
+ position: absolute;
329
+ width: 34px;
330
+ height: 34px;
331
+ border-radius: 50%;
332
+ background-color: rgba(0, 0, 0, 0.5);
333
+ pointer-events: none;
334
+ z-index: 2147483611;
335
+ animation: click-wave-animation 0.8s ease forwards;
336
+ }
337
+
338
+ @keyframes click-wave-animation {
339
+ 0% {
340
+ transform: scale(0.2);
341
+ opacity: 1;
342
+ }
343
+ 100% {
344
+ transform: scale(2);
345
+ opacity: 0;
346
+ }
347
+ }
348
+
349
+ ${
350
+ this . productTourData . gradient
351
+ ? `body::before {
311
352
content: "";
312
353
position: fixed;
313
354
top: 0;
@@ -317,11 +358,10 @@ export default class GleapCopilotTours {
317
358
pointer-events: all;
318
359
z-index: 2147483610;
319
360
box-sizing: border-box;
320
- border: 8px solid transparent;
321
- filter: blur(20px );
361
+ border: 20px solid transparent;
362
+ filter: blur(28px );
322
363
border-image-slice: 1;
323
- border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
324
- animation: animateBorder 3s infinite alternate ease-in-out;
364
+ border-image-source: linear-gradient(45deg, #ED5587, #FBE6A9, #a6e3f8, #C294F2);
325
365
}
326
366
327
367
body::after {
@@ -337,46 +377,9 @@ export default class GleapCopilotTours {
337
377
box-sizing: border-box;
338
378
border: 2px solid transparent;
339
379
border-image-slice: 1;
340
- border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
341
- animation: animateBorder 3s infinite alternate ease-in-out;
342
- }
343
-
344
- @keyframes animateBorder {
345
- 0% {
346
- border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
347
- }
348
- 50% {
349
- border-image-source: linear-gradient(135deg, #e721b3, #ff8a00);
350
- }
351
- 100% {
352
- border-image-source: linear-gradient(225deg, #ff8a00, #2142e7);
353
- }
354
- }
355
-
356
- .copilot-info-container {
357
- position: fixed;
358
- top: 20px;
359
- right: 20px;
360
- z-index: 2147483610;
361
- background: #fff;
362
- padding: 5px;
363
- padding-left: 10px;
364
- border-radius: 10px;
365
- box-shadow: 0 0 20px 0 #e721b263;
366
- font-family: sans-serif;
367
- font-size: 13px;
368
- color: #000;
369
- display: flex;
370
- align-items: center;
371
- gap: 10px;
372
- border: 1px solid #e721b3;
373
- max-width: min(330px, 100vw - 40px);
374
- }
375
-
376
- .copilot-info-container svg {
377
- width: 24px;
378
- height: 24px;
379
- flex-shrink: 0;
380
+ border-image-source: linear-gradient(45deg, #ED5587, #FBE6A9, #a6e3f8, #C294F2);
381
+ }`
382
+ : ""
380
383
}
381
384
` ;
382
385
document . head . appendChild ( styleNode ) ;
@@ -416,6 +419,8 @@ export default class GleapCopilotTours {
416
419
// Setup the copilot tour.
417
420
this . setupCopilotTour ( ) ;
418
421
422
+ // Show copilot joined info.
423
+
419
424
// Render the first step.
420
425
this . renderNextStep ( ) ;
421
426
}
@@ -427,12 +432,37 @@ export default class GleapCopilotTours {
427
432
// Check if we have reached the end of the tour.
428
433
if ( this . currentActiveIndex >= steps . length ) {
429
434
this . cleanup ( ) ;
435
+ if ( this . onCompleteCallback ) {
436
+ this . onCompleteCallback ( ) ;
437
+ }
430
438
return ;
431
439
}
432
440
433
441
const currentStep = steps [ this . currentActiveIndex ] ;
434
442
435
443
const handleStep = ( element ) => {
444
+ const gotToNextStep = ( ) => {
445
+ this . currentActiveIndex ++ ;
446
+ this . storeUncompletedTour ( ) ;
447
+
448
+ if ( currentStep . mode === "CLICK" && element ) {
449
+ const rect = element . getBoundingClientRect ( ) ;
450
+
451
+ // Get current scroll position.
452
+ const scrollX = window . scrollX || 0 ;
453
+ const scrollY = window . scrollY || 0 ;
454
+
455
+ performClickAnimation (
456
+ rect . left + rect . width / 2 + scrollX ,
457
+ rect . top + rect . height / 2 + scrollY
458
+ ) ;
459
+
460
+ element . click ( ) ;
461
+ }
462
+
463
+ this . renderNextStep ( ) ;
464
+ } ;
465
+
436
466
// Update pointer position, even if element is null.
437
467
this . updatePointerPosition ( element ) ;
438
468
@@ -447,21 +477,32 @@ export default class GleapCopilotTours {
447
477
// Estimate read time in seconds.
448
478
const readTime = estimateReadTime ( message ) ;
449
479
450
- // Automatically move to the next step after the estimated read time.
451
- setTimeout ( ( ) => {
452
- this . currentActiveIndex ++ ;
453
- this . storeUncompletedTour ( ) ;
454
-
455
- if ( currentStep . mode === "CLICK" && element ) {
456
- try {
457
- element . click ( ) ;
458
- } catch ( e ) {
459
- console . error ( "Error clicking the element:" , e ) ;
460
- }
480
+ console . log ( "Read time:" , currentStep ) ;
481
+
482
+ // Read the message.
483
+ if ( currentStep . voice && currentStep . voice . length > 0 ) {
484
+ try {
485
+ const audio = new Audio ( currentStep . voice ) ;
486
+
487
+ // Add an event listener for the 'ended' event
488
+ audio . addEventListener ( "ended" , ( ) => {
489
+ setTimeout ( ( ) => {
490
+ gotToNextStep ( ) ;
491
+ } , 1000 ) ;
492
+ } ) ;
493
+
494
+ // Play the audio
495
+ audio . play ( ) ;
496
+ } catch ( error ) {
497
+ setTimeout ( ( ) => {
498
+ gotToNextStep ( ) ;
499
+ } , readTime * 1000 ) ;
461
500
}
462
-
463
- this . renderNextStep ( ) ;
464
- } , readTime * 1000 ) ;
501
+ } else {
502
+ setTimeout ( ( ) => {
503
+ gotToNextStep ( ) ;
504
+ } , readTime * 1000 ) ;
505
+ }
465
506
} ;
466
507
467
508
const elementPromise = currentStep . selector
0 commit comments