@@ -4,10 +4,10 @@ const styleId = "copilot-tour-styles";
44const copilotInfoContainerId = "copilot-info-container" ;
55
66function estimateReadTime ( text ) {
7- const wordsPerSecond = 3.5 ; // Average reading speed
7+ const wordsPerSecond = 3.6 ; // Average reading speed
88 const wordCount = text . split ( / \s + / ) . filter ( ( word ) => word . length > 0 ) . length ;
99 const readTimeInSeconds = Math . ceil ( wordCount / wordsPerSecond ) ;
10- return readTimeInSeconds + 1.5 ;
10+ return readTimeInSeconds + 1 ;
1111}
1212
1313function htmlToPlainText ( html ) {
@@ -26,6 +26,26 @@ function scrollToElement(element) {
2626 }
2727}
2828
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+
2949function waitForElement ( selector , timeout = 5000 ) {
3050 const pollInterval = 100 ;
3151 const maxAttempts = timeout / pollInterval ;
@@ -62,6 +82,7 @@ export default class GleapCopilotTours {
6282 currentActiveIndex = undefined ;
6383 lastArrowPositionX = undefined ;
6484 lastArrowPositionY = undefined ;
85+ onCompleteCallback = undefined ;
6586
6687 // GleapReplayRecorder singleton
6788 static instance ;
@@ -102,7 +123,7 @@ export default class GleapCopilotTours {
102123 } ) ;
103124 }
104125
105- startWithConfig ( tourId , config , delay = 0 ) {
126+ startWithConfig ( tourId , config , onCompleteCallback = undefined ) {
106127 // Prevent multiple tours from being started.
107128 if ( this . productTourId ) {
108129 return ;
@@ -111,27 +132,8 @@ export default class GleapCopilotTours {
111132 this . productTourId = tourId ;
112133 this . productTourData = config ;
113134 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 ( ) ;
135137 }
136138
137139 storeUncompletedTour ( ) {
@@ -154,14 +156,8 @@ export default class GleapCopilotTours {
154156 localStorage . setItem ( localStorageKey , JSON . stringify ( data ) ) ;
155157 } catch ( e ) { }
156158 } else {
157- this . clearUncompletedTour ( ) ;
158- }
159- }
160-
161- clearUncompletedTour ( ) {
162- try {
163159 localStorage . removeItem ( localStorageKey ) ;
164- } catch ( e ) { }
160+ }
165161 }
166162
167163 updatePointerPosition ( anchor ) {
@@ -199,7 +195,7 @@ export default class GleapCopilotTours {
199195 let anchorCenterY =
200196 anchorRect . top + anchorRect . height / 2 + window . scrollY ;
201197
202- let containerWidthSpace = 330 ;
198+ let containerWidthSpace = 350 ;
203199 if ( containerWidthSpace > window . innerWidth - 40 ) {
204200 containerWidthSpace = window . innerWidth - 40 ;
205201 }
@@ -230,29 +226,24 @@ export default class GleapCopilotTours {
230226 }
231227
232228 cleanup ( ) {
233- this . removePointerUI ( ) ;
234- this . clearUncompletedTour ( ) ;
235- }
236-
237- removePointerUI ( ) {
238229 const container = document . getElementById ( pointerContainerId ) ;
239230 if ( container ) {
240231 container . remove ( ) ;
241232 }
242233
243- // Remove style node.
244- const styleNode = document . getElementById ( styleId ) ;
245- if ( styleNode ) {
246- styleNode . remove ( ) ;
247- }
248-
249- // Remove copilot info container.
250234 const copilotInfoContainer = document . getElementById (
251235 copilotInfoContainerId
252236 ) ;
253237 if ( copilotInfoContainer ) {
254238 copilotInfoContainer . remove ( ) ;
255239 }
240+
241+ setTimeout ( ( ) => {
242+ const styleNode = document . getElementById ( styleId ) ;
243+ if ( styleNode ) {
244+ styleNode . remove ( ) ;
245+ }
246+ } , 1000 ) ;
256247 }
257248
258249 setupCopilotTour ( ) {
@@ -279,7 +270,7 @@ export default class GleapCopilotTours {
279270 height: auto;
280271 fill: none;
281272 }
282-
273+
283274 .${ pointerContainerId } -right {
284275 left: auto;
285276 right: 0;
@@ -307,7 +298,57 @@ export default class GleapCopilotTours {
307298 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
308299 }
309300
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 {
311352 content: "";
312353 position: fixed;
313354 top: 0;
@@ -317,11 +358,10 @@ export default class GleapCopilotTours {
317358 pointer-events: all;
318359 z-index: 2147483610;
319360 box-sizing: border-box;
320- border: 8px solid transparent;
321- filter: blur(20px );
361+ border: 20px solid transparent;
362+ filter: blur(28px );
322363 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);
325365 }
326366
327367 body::after {
@@ -337,46 +377,9 @@ export default class GleapCopilotTours {
337377 box-sizing: border-box;
338378 border: 2px solid transparent;
339379 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+ : ""
380383 }
381384 ` ;
382385 document . head . appendChild ( styleNode ) ;
@@ -416,6 +419,8 @@ export default class GleapCopilotTours {
416419 // Setup the copilot tour.
417420 this . setupCopilotTour ( ) ;
418421
422+ // Show copilot joined info.
423+
419424 // Render the first step.
420425 this . renderNextStep ( ) ;
421426 }
@@ -427,12 +432,37 @@ export default class GleapCopilotTours {
427432 // Check if we have reached the end of the tour.
428433 if ( this . currentActiveIndex >= steps . length ) {
429434 this . cleanup ( ) ;
435+ if ( this . onCompleteCallback ) {
436+ this . onCompleteCallback ( ) ;
437+ }
430438 return ;
431439 }
432440
433441 const currentStep = steps [ this . currentActiveIndex ] ;
434442
435443 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+
436466 // Update pointer position, even if element is null.
437467 this . updatePointerPosition ( element ) ;
438468
@@ -447,21 +477,32 @@ export default class GleapCopilotTours {
447477 // Estimate read time in seconds.
448478 const readTime = estimateReadTime ( message ) ;
449479
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 ) ;
461500 }
462-
463- this . renderNextStep ( ) ;
464- } , readTime * 1000 ) ;
501+ } else {
502+ setTimeout ( ( ) => {
503+ gotToNextStep ( ) ;
504+ } , readTime * 1000 ) ;
505+ }
465506 } ;
466507
467508 const elementPromise = currentStep . selector
0 commit comments