Repository: Ontotext-AD/graphdb-workbench
Pull request #1880: GDB-11877 Implement interactive guide autostart
Author: login: yordanalexandrov
Created: 2025-03-12T10:40:31Z | Merged: 2025-03-13T18:07:31Z

## What
Implement logic to autostart specific guides when user opens the workbench with a query parameter `autostartGuide` pointing to a guideId

## Why
This will allow to generate links which will automatically start a specific guide. This is required by the GDB Sandbox project, where users are given a link which will open the workbench and start a guide

## How
- add new LSKeys for storing a flag which allows auto started guide to be disabled upon completion or upon user request
- add logic in `mainCtrl` to check for the query param `autostartGuide` and if found to call `GuidesService.autoStartGuide`
- add new method `GuidesService.autoStartGuide` to fetch the specified guide and run it
- refactored some logic in `ShepherdService`
  - removed default alert when canceling the guide and replaced it with a new custom modal
  - add checks if the auto start of the guide is disabled
  - changed the cancellation logic a bit

## Testing
N/A

## Screenshots
![image](https://github.com/user-attachments/assets/f1900ed8-6a14-4f43-8f83-68de9caa4504)
![image](https://github.com/user-attachments/assets/bb92e7b4-414a-41e0-bf39-ee9ea94d303d)


## Checklist
- [x] Branch name
- [x] Target branch
- [x] Commit messages
- [x] Squash commits
- [x] MR name
- [x] MR Description
- [ ] Tests



Diff:
diff --git a/src/i18n/locale-en.json b/src/i18n/locale-en.json
index 3bbe6a5d5b..5c13ce9b7b 100644
--- a/src/i18n/locale-en.json
+++ b/src/i18n/locale-en.json
@@ -3097,6 +3097,10 @@
     "repo.page.location.input.field.tooltip": "The location where to create repository. The default is the local one.",
     "repo.page.repository.info.loading.error": "Currently selected repository cannot be edited due to an error during repository metadata loading.",
     "guides.loading.message.failure": "Failed to load guides!",
+    "guides.confirm-cancel-modal.title": "Exit the guide?",
+    "guides.confirm-cancel-modal.content": "Are you sure you want to exit the guide?<br><br>You can start it anytime from <b>Help > Interactive Guides</b>",
+    "guides.confirm-cancel-modal.exit-button": "Exit",
+    "guides.confirm-cancel-modal.dont-show-button": "Don't start automatically",
     "guide.button.guide-paused": "Guide paused",
     "guide.button.guide-resume": "Continue the guide",
     "guide.validate.no-next": "Cannot continue to next",
diff --git a/src/i18n/locale-fr.json b/src/i18n/locale-fr.json
index 2c2c7e0c2b..1ae43d58e8 100644
--- a/src/i18n/locale-fr.json
+++ b/src/i18n/locale-fr.json
@@ -3095,6 +3095,10 @@
     "repo.page.location.input.field.tooltip": "L'emplacement où créer le dépôt. Par défaut, il s'agit de l'emplacement local.",
     "repo.page.repository.info.loading.error": "Le référentiel actuellement sélectionné ne peut pas être modifié en raison d'une erreur lors du chargement des métadonnées du référentiel.",
     "guides.loading.message.failure": "Échec du chargement des guides!",
+    "guides.confirm-cancel-modal.title": "Quitter le guide?",
+    "guides.confirm-cancel-modal.content": "Êtes-vous sûr de vouloir quitter le guide ?<br><br>Vous pouvez le lancer à tout moment à partir de <b>Aide > Guides interactifs</b>.",
+    "guides.confirm-cancel-modal.exit-button": "Sortie",
+    "guides.confirm-cancel-modal.dont-show-button": "Ne pas démarrer automatiquement",
     "guide.button.guide-paused": "Guide en pause",
     "guide.button.guide-resume": "Continuer le guide",
     "guide.validate.no-next": "Impossible de passer au suivant",
diff --git a/src/js/angular/controllers.js b/src/js/angular/controllers.js
index 8bd6eaa4bd..16d9573e5b 100644
--- a/src/js/angular/controllers.js
+++ b/src/js/angular/controllers.js
@@ -185,9 +185,20 @@ function mainCtrl($scope, $menuItems, $jwtAuth, $http, toastr, $location, $repos
 
     $scope.hideRdfResourceSearch = false;
     $scope.showRdfResourceSearch = () => {
-        return !$scope.hideRdfResourceSearch && !!$scope.getActiveRepository() && $scope.hasActiveLocation() &&(!$scope.isLoadingLocation() || $scope.isLoadingLocation() && $location.url() === '/repository');
+        return !$scope.hideRdfResourceSearch && !!$scope.getActiveRepository() && $scope.hasActiveLocation() && (!$scope.isLoadingLocation() || $scope.isLoadingLocation() && $location.url() === '/repository');
     };
 
+    const queryParams = $location.search();
+    if (queryParams.autostartGuide) {
+        // Check to see if $translate service is ready with the language before starting the guide as the steps are translated ahead on time. Will retry 20 times (1 second).
+        const timer = $interval(function () {
+            if ($translate.use()) {
+                GuidesService.autoStartGuide(queryParams.autostartGuide)
+                $interval.cancel(timer);
+            }
+        }, 50, 20);
+    }
+
     $scope.onRdfResourceSearch = () => {
         if (isHomePage()) {
             $scope.hideRdfResourceSearch = true;
@@ -700,10 +711,10 @@ function mainCtrl($scope, $menuItems, $jwtAuth, $http, toastr, $location, $repos
     }
 
     $scope.closeActiveRepoPopover = function () {
-       $scope.isActiveRepoPopoverOpen = false;
+        $scope.isActiveRepoPopoverOpen = false;
     };
 
-    const closeActiveRepoPopoverEventHandler = function(event) {
+    const closeActiveRepoPopoverEventHandler = function (event) {
         const popoverElement = document.querySelector('.popover');
         if ($scope.isActiveRepoPopoverOpen && popoverElement && !popoverElement.contains(event.target)) {
             $timeout(function () {
diff --git a/src/js/angular/guides/controllers.js b/src/js/angular/guides/controllers.js
index 443adabbcc..d690eea999 100644
--- a/src/js/angular/guides/controllers.js
+++ b/src/js/angular/guides/controllers.js
@@ -13,9 +13,9 @@ angular
     .module('graphdb.framework.guides.controllers', modules)
     .controller('GuidesCtrl', GuidesCtrl);
 
-GuidesCtrl.$inject = ['$scope', '$rootScope', 'GuidesService', '$filter', '$translate', '$interpolate', 'toastr'];
+GuidesCtrl.$inject = ['$scope', '$rootScope', 'GuidesService', '$filter', '$translate', '$interpolate'];
 
-function GuidesCtrl($scope, $rootScope, GuidesService, $filter, $translate, $interpolate, toastr) {
+function GuidesCtrl($scope, $rootScope, GuidesService, $filter, $translate, $interpolate) {
 
     // =========================
     // Public variables
diff --git a/src/js/angular/guides/guides.service.js b/src/js/angular/guides/guides.service.js
index ad1b6b68ff..6c25ce4084 100644
--- a/src/js/angular/guides/guides.service.js
+++ b/src/js/angular/guides/guides.service.js
@@ -209,8 +209,8 @@ function GuidesService(
      * @param {*} guide - the guide to start as an object.
      * @param {*} startStepId
      */
-    this.startGuide = (guide, startStepId) => {
-        if (guide && guide.options && guide.options.repositoryIdBase) {
+    this.startGuide = (guide, startStepId, isAutoStarted = false) => {
+        if (guide?.options?.repositoryIdBase) {
             // repositoryIdBase in the options can be used as a template to find a free repository ID.
             // For example, setting repositoryIdBase to 'myrepo' will find the first free ID from:
             // - myrepo
@@ -236,12 +236,34 @@ function GuidesService(
         if (angular.isDefined(startStepId)) {
             ShepherdService.resumeGuide(guide.guideId, stepsDescriptions, startStepId);
         } else {
-            ShepherdService.startGuide(guide.guideId, stepsDescriptions, startStepId);
+            ShepherdService.startGuide(guide.guideId, stepsDescriptions, startStepId, isAutoStarted);
         }
 
         $rootScope.$broadcast('guideStarted');
     };
 
+    /**
+     * Starts a guide by give guide id and marks is as auto started
+     *
+     * @param {string | number} autoStartGuideId
+     */
+    this.autoStartGuide = (autoStartGuideId) => {
+        if (!angular.isString(autoStartGuideId) && !angular.isNumber(autoStartGuideId)) {
+            return;
+        }
+
+        if (angular.isNumber(autoStartGuideId)) {
+            autoStartGuideId = autoStartGuideId + '';
+        }
+
+        this.getGuides().then((guides) => {
+            const selectedGuide = guides.find((guide) => guide.guideId === autoStartGuideId);
+            if (selectedGuide) {
+                this.startGuide(selectedGuide, undefined, true);
+            }
+        });
+    }
+
     /**
      * Fetches list with all available guides.
      * @return {Promise<[]>} array with guides descriptions. A guide description have to be:
@@ -266,7 +288,10 @@ function GuidesService(
                     if (guide.guideId === undefined) {
                         // Ideally we want our guides to have stable IDs but it's not a big deal
                         // if they don't have one - so just generate it in that case
-                        guide.guideId = Date.now() + index;
+                        const id = Date.now() + index;
+                        guide.guideId = id + '';
+                    } else if (angular.isNumber(guide.guideId)) {
+                        guide.guideId = guide.guideId + '';
                     }
                 });
                 return guides;
@@ -434,7 +459,20 @@ function GuidesService(
      * @private
      */
     this._getSteps = (complexStep, parentOptions) => {
-        const services = {$translate, $interpolate, GuideUtils, $rootScope, toastr, $location, $route, $timeout, ShepherdService, $repositories, YasguiComponentDirectiveUtil, EventEmitterService};
+        const services = {
+            $translate,
+            $interpolate,
+            GuideUtils,
+            $rootScope,
+            toastr,
+            $location,
+            $route,
+            $timeout,
+            ShepherdService,
+            $repositories,
+            YasguiComponentDirectiveUtil,
+            EventEmitterService
+        };
         let steps = [];
         if (angular.isArray(complexStep)) {
             complexStep.forEach((stepDescription) => {
diff --git a/src/js/angular/guides/steps/core/plugin.js b/src/js/angular/guides/steps/core/plugin.js
index 94197db8cd..625d20a4f8 100644
--- a/src/js/angular/guides/steps/core/plugin.js
+++ b/src/js/angular/guides/steps/core/plugin.js
@@ -144,7 +144,10 @@ PluginRegistry.add('guide.step', [
         getStep: (options, services) => {
             return angular.extend({}, BASIC_STEP, {
                 initPreviousStep: services.GuideUtils.defaultInitPreviousStep,
-                beforeShowPromise: () => services.GuideUtils.waitUntilHidden(options.elementSelectorToHide, options.timeToWait || 2),
+                beforeShowPromise: (guide) => services.GuideUtils.waitUntilHidden(options.elementSelectorToHide, options.timeToWait || 2)
+                    .catch(() => {
+                        services.ShepherdService._abortGuide(guide)
+                    }),
                 show: (guide) => () => {
                     // Using a timeout because the library executes async logic
                     setTimeout(() => guide.next())
@@ -157,7 +160,10 @@ PluginRegistry.add('guide.step', [
         getStep: (options, services) => {
             return angular.extend({}, BASIC_STEP, {
                 initPreviousStep: services.GuideUtils.defaultInitPreviousStep,
-                beforeShowPromise: () => services.GuideUtils.getOrWaitFor(options.elementSelectorToShow, options.timeToWait || 2),
+                beforeShowPromise: (guide) => services.GuideUtils.getOrWaitFor(options.elementSelectorToShow, options.timeToWait || 2)
+                    .catch(() => {
+                        services.ShepherdService._abortGuide(guide)
+                    }),
                 show: (guide) => () => {
                     // Using a timeout because the library executes async logic
                     setTimeout(() => guide.next())
@@ -172,13 +178,7 @@ PluginRegistry.add('guide.step', [
                 type: 'readonly',
                 title: options.title || 'guide.step_plugin.guide-ended.title',
                 content: options.content || 'guide.step_plugin.guide-ended.content',
-                lastStep: true,
-                show: (guide) => () => {
-                    guide.options.confirmCancel = false;
-                },
-                hide: (guide) => () => {
-                    guide.options.confirmCancel = true;
-                }
+                lastStep: true
             };
             return angular.extend({}, BASIC_STEP, {
                 initPreviousStep: services.GuideUtils.defaultInitPreviousStep
diff --git a/src/js/angular/guides/templates/modal-confirm-exit.html b/src/js/angular/guides/templates/modal-confirm-exit.html
new file mode 100644
index 0000000000..121b04c532
--- /dev/null
+++ b/src/js/angular/guides/templates/modal-confirm-exit.html
@@ -0,0 +1,13 @@
+<div class="modal-header">
+	<button ng-click="cancel();" class="close" aria-hidden="true"></button>
+    <h3 class="modal-title">{{title}}</h3>
+</div>
+<div class="modal-body">
+    <div class="lead" ng-bind-html="message">
+    </div>
+</div>
+<div class="modal-footer">
+    <button type="button" class="btn btn-secondary cancel-btn" ng-click="cancel()">{{'common.cancel.btn' | translate}}</button>
+    <button ng-if="hasDontShowAgain" type="button" class="btn btn-secondary cancel-btn" ng-click="dontShowAgain()">{{'guides.confirm-cancel-modal.dont-show-button' | translate}}</button>
+    <button class="btn btn-primary confirm-btn" ng-click="exit()">{{ ('guides.confirm-cancel-modal.exit-button') | translate}}</button>
+</div>
diff --git a/src/js/angular/guides/tour-lib-services/shepherd.service.js b/src/js/angular/guides/tour-lib-services/shepherd.service.js
index 12c4ccd6e5..bdfbea48dc 100644
--- a/src/js/angular/guides/tour-lib-services/shepherd.service.js
+++ b/src/js/angular/guides/tour-lib-services/shepherd.service.js
@@ -1,5 +1,6 @@
 import {GuideUtils} from "../guide-utils";
 import Shepherd from "shepherd.js";
+import {decodeHTML} from "../../../../app";
 
 export const GUIDE_ID = 'shepherd.guide_id';
 export const GUIDE_CURRENT_STEP_ID = 'shepherd.guide.current.step.id';
@@ -11,18 +12,27 @@ const modules = ['graphdb.framework.utils.localstorageadapter'];
 angular
     .module('graphdb.framework.guides.shepherd.services', modules)
     .service('ShepherdService', ShepherdService);
+
+ShepherdService.$inject = ['$location', '$translate', 'LocalStorageAdapter', 'LSKeys', '$route', '$interpolate', 'toastr', '$compile', 'ModalService'];
+
 /**
  * Service (wrapper) of library  <a href="https://shepherdjs.dev/docs/">shepherd</a>.
  * @param $location
  * @param $translate
  * @param LocalStorageAdapter
+ * @param LSKeys
  * @param $route
+ * @param $interpolate
+ * @param toastr
+ * @param $compile
+ * @param ModalService
  * @constructor
  */
-ShepherdService.$inject = ['$location', '$translate', 'LocalStorageAdapter', '$route', '$interpolate', 'toastr', '$compile'];
-
-function ShepherdService($location, $translate, LocalStorageAdapter, $route, $interpolate, toastr, $compile) {
+function ShepherdService($location, $translate, LocalStorageAdapter, LSKeys, $route, $interpolate, toastr, $compile, ModalService) {
     this.guideCancelSubscription = undefined;
+    this.guideCompleteSubscription = undefined;
+    this.guideAutostarted = null;
+
     this.onPause = () => {
     };
     this.onCancel = () => {
@@ -115,9 +125,16 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
      *
      * </ul>
      * @param {string} startStepId - start step id.
+     * @param isAutoStarted - whether the guide was automatically started
      * @param {boolean} clearHistory - if true will clear guide step history.
      */
-    this.startGuide = (guideId, stepsDescriptions, startStepId, clearHistory = true) => {
+    this.startGuide = (guideId, stepsDescriptions, startStepId, isAutoStarted = false, clearHistory = true) => {
+        if (isAutoStarted && this._isAutostartDisabledForGuide(guideId)) {
+            return;
+        } else {
+            this.guideAutostarted = isAutoStarted;
+        }
+
         if (clearHistory) {
             LocalStorageAdapter.remove(GUIDE_STEP_HISTORY);
         }
@@ -169,6 +186,17 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
         }
     };
 
+    this._isAutostartDisabledForGuide = (guideId) => {
+        const guideAutostartState = LocalStorageAdapter.get(LSKeys.GUIDES_AUTOSTART) || {};
+        return guideAutostartState[guideId]?.disabled;
+    }
+
+    this._disableAutostartForGuide = (guideId) => {
+        const guideAutostartState = LocalStorageAdapter.get(LSKeys.GUIDES_AUTOSTART) || {};
+        guideAutostartState[guideId] = {disabled: true}
+        LocalStorageAdapter.set(LSKeys.GUIDES_AUTOSTART, JSON.stringify(guideAutostartState));
+    }
+
     /**
      * Creates a guide.
      * @param {string} guideId - a unique ID that identifies the guide.
@@ -179,8 +207,7 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
         return new Shepherd.Tour({
             name: guideId,
             useModalOverlay: true,
-            confirmCancel: true,
-            confirmCancelMessage: $translate.instant('guide.confirm.cancel.message'),
+            confirmCancel: this._confirmGuideCancel,
             defaultStepOptions: {
                 classes: 'shadow-md bg-purple-dark',
                 scrollTo: true
@@ -188,6 +215,57 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
         });
     };
 
+    this._confirmGuideCancel = () => {
+        const guide = Shepherd.activeTour;
+        const currentStep = guide.getCurrentStep()
+
+        if (currentStep.options.isLastStep) {
+            this._completeGuide(guide);
+            return;
+        }
+
+        guide.hide();
+
+        return ModalService.openCustomModal({
+            templateUrl: 'js/angular/guides/templates/modal-confirm-exit.html',
+            title: decodeHTML($translate.instant('guides.confirm-cancel-modal.title')),
+            message: decodeHTML($translate.instant('guides.confirm-cancel-modal.content')),
+            warning: true,
+            controller: ['$scope', '$uibModalInstance', '$sce', 'config', ConfirmExitModalCtrl],
+            hasDontShowAgain: this.guideAutostarted,
+        }).result
+            .then((disableAutoStart) => {
+                // Exit of guide is confirmed
+                if (disableAutoStart) {
+                    // User choose not to auto start the guide again
+                    this._disableAutostartForGuide(guide.options.name)
+                }
+                return true
+            })
+            .catch(() => {
+                guide.show(currentStep.id);
+            });
+
+        function ConfirmExitModalCtrl($scope, $uibModalInstance, $sce, config) {
+            $scope.cancelButtonKey = config.cancelButtonKey;
+            $scope.hasDontShowAgain = config.hasDontShowAgain;
+            $scope.title = config.title;
+            $scope.message = $sce.trustAsHtml(config.message);
+
+            $scope.exit = function () {
+                $uibModalInstance.close(false);
+            };
+
+            $scope.dontShowAgain = function () {
+                $uibModalInstance.close(true);
+            };
+
+            $scope.cancel = function () {
+                $uibModalInstance.dismiss();
+            };
+        }
+    }
+
     /**
      * Transforms <code>stepsDescriptions</code> to Shepherd Tour steps and add it to <code>guide<code>.
      * @param {Shepherd.Tour} guide - the guide.
@@ -332,20 +410,25 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
     };
 
     this._subscribeToGuideCanceled = () => {
+        const guideEndHandler = ({tour}) => {
+            this._clearLocalStorage();
+            const currentStep = tour.getCurrentStep();
+            if (currentStep && angular.isFunction(currentStep.hide)) {
+                // Some steps have state and when shepherd library hides them the "hide()" function is called. This function cleaned the state
+                // of the steps. When we are on step which has state and click on cancel button the library don't call this function.
+                // So we call it manually. For example see "select-repository" plugin it has listener which must be removed when step is hided.
+                currentStep.hide();
+            }
+            this.guideAutostarted = null;
+            if (this.onCancel) {
+                this.onCancel();
+            }
+        }
         if (!this.guideCancelSubscription) {
-            this.guideCancelSubscription = Shepherd.on('cancel', () => {
-                this._clearLocaleStorage();
-                const currentStep = Shepherd.activeTour.getCurrentStep();
-                if (currentStep && angular.isFunction(currentStep.hide)) {
-                    // Some steps have state and when shepherd library hides them the "hide()" function is called. This function cleaned the state
-                    // of the steps. When we are on step which has state and click on cancel button the library don't call this function.
-                    // So we call it manually. For example see "select-repository" plugin it has listener which must be removed when step is hided.
-                    currentStep.hide();
-                }
-                if (this.onCancel) {
-                    this.onCancel();
-                }
-            });
+            this.guideCancelSubscription = Shepherd.on('cancel', guideEndHandler);
+        }
+        if (!this.guideCompleteSubscription) {
+            this.guideCompleteSubscription = Shepherd.on('complete', guideEndHandler);
         }
     };
 
@@ -397,27 +480,35 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
 
 
     this._getCancelButton = (guide) => {
-        return this._getButton($translate.instant('common.close'), () => this._cancelGuide(guide, false), true);
+        return this._getButton($translate.instant('common.close'), () => this._completeGuide(guide), true);
     };
 
     this._backToGuidesButton = (guide) => {
         $location.path("guides");
-        this._cancelGuide(guide, false);
+        this._completeGuide(guide);
     };
 
     /**
-     * Cancel the guide.
+     * Completes teh guide
+     * @param guide
+     * @private
+     */
+    this._completeGuide = (guide) => {
+        if (this.guideAutostarted) {
+            this._disableAutostartForGuide(guide.options.name)
+        }
+        guide.complete();
+    }
+
+    /**
+     * Abort the guide due to an error.
      * @param {Shepherd.Tour} guide
-     * @param {boolean} confirmCancel
      * @private
      */
-    this._cancelGuide = (guide, confirmCancel = true) => {
-        guide.options.confirmCancel = confirmCancel;
+    this._abortGuide = (guide) => {
+        toastr.error($translate.instant('guide.unexpected.error.message'));
+        guide.options.confirmCancel = false;
         guide.cancel();
-
-        if (this.onCancel()) {
-            this.onCancel();
-        }
     };
 
     /**
@@ -571,8 +662,7 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
                             })
                             .catch((error) => {
                                 console.log(error);
-                                toastr.error($translate.instant('guide.unexpected.error.message'));
-                                this._cancelGuide(guide, false);
+                                this._abortGuide(guide);
                             });
                         return;
                     } else if (nextStep.options.forceReload || nextStep.options.url && nextStep.options.url !== currentStep.options.url) {
@@ -583,8 +673,7 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
                 })
                 .catch((error) => {
                     console.log(error);
-                    toastr.error($translate.instant('guide.unexpected.error.message'));
-                    this._cancelGuide(guide, false);
+                    this._abortGuide(guide);
                 });
         };
     };
@@ -611,10 +700,7 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
                         if (angular.isFunction(currentStepDescription.onNextClick)) {
                             const onNextResult = currentStepDescription.onNextClick(guide, currentStepDescription);
                             if (onNextResult instanceof Promise) {
-                                onNextResult.catch((error) => {
-                                    toastr.error($translate.instant('guide.unexpected.error.message'));
-                                    this._cancelGuide(guide, false);
-                                });
+                                onNextResult.catch(() => this._abortGuide(guide));
                             }
                         } else {
                             if (nextStepDescription.forceReload || nextStepDescription.url && nextStepDescription.url !== currentStepDescription.url) {
@@ -709,7 +795,7 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
      * Remove all related data to a guid from local store.
      * @private
      */
-    this._clearLocaleStorage = () => {
+    this._clearLocalStorage = () => {
         LocalStorageAdapter.remove(GUIDE_ID);
         LocalStorageAdapter.remove(GUIDE_PAUSE);
         LocalStorageAdapter.remove(GUIDE_CURRENT_STEP_ID);
@@ -789,6 +875,7 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
             onPreviousClick: stepDescription.onPreviousClick,
             initPreviousStep: stepDescription.initPreviousStep,
             forceReload: stepDescription.forceReload,
+            isLastStep: !!stepDescription.lastStep,
             when: {
                 show: this._getShowFunction(guide, stepDescription, onShow)
             }
diff --git a/src/js/angular/utils/local-storage-adapter.js b/src/js/angular/utils/local-storage-adapter.js
index dc8783b2f6..7399ffb733 100644
--- a/src/js/angular/utils/local-storage-adapter.js
+++ b/src/js/angular/utils/local-storage-adapter.js
@@ -30,7 +30,8 @@ angular
         'IMPORT_VIEW': 'import-view',
         'SPARQL_LAST_REPO': 'sparql-last-repo',
         'TTYG': 'ttyg',
-        'COOKIE_CONSENT': 'cookie-consent'
+        'COOKIE_CONSENT': 'cookie-consent',
+        'GUIDES_AUTOSTART': 'guides-autostart'
     });
 
 LocalStorageAdapter.$inject = ['localStorageService', 'LSKeys'];
diff --git a/test-cypress/fixtures/locale-en.json b/test-cypress/fixtures/locale-en.json
index b33f8b5e61..8d1087e5d6 100644
--- a/test-cypress/fixtures/locale-en.json
+++ b/test-cypress/fixtures/locale-en.json
@@ -3125,6 +3125,10 @@
     "repo.page.location.input.field.tooltip": "The location where to create repository. The default is the local one.",
     "repo.page.repository.info.loading.error": "Currently selected repository cannot be edited due to an error during repository metadata loading.",
     "guides.loading.message.failure": "Failed to load guides!",
+    "guides.confirm-cancel-modal.title": "Exit the guide?",
+    "guides.confirm-cancel-modal.content": "Are you sure you want to exit the guide?<br><br>You can start it anytime from <b>Help > Interactive Guides</b>",
+    "guides.confirm-cancel-modal.exit-button": "Exit",
+    "guides.confirm-cancel-modal.dont-show-button": "Don't start automatically",
     "guide.button.guide-paused": "Guide paused",
     "guide.button.guide-resume": "Continue the guide",
     "guide.validate.no-next": "Cannot continue to next",
diff --git a/webpack.config.common.js b/webpack.config.common.js
index 29977d2ca5..c4e890c806 100644
--- a/webpack.config.common.js
+++ b/webpack.config.common.js
@@ -222,6 +222,10 @@ module.exports = {
                 from: 'src/js/angular/templates',
                 to: 'js/angular/templates'
             },
+            {
+                from: 'src/js/angular/guides/templates',
+                to: 'js/angular/guides/templates'
+            },
             {
                 from: 'node_modules/ontotext-graphql-playground-component/dist/ontotext-graphql-playground-component/assets',
                 to: 'assets'


Reviews:
user: login: svilenvelikov
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T11:50:34Z
user: login: yordanalexandrov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T13:04:31Z
user: login: yordanalexandrov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T13:06:55Z
user: login: yordanalexandrov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T09:26:41Z
user: login: yordanalexandrov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:27:04Z
user: login: svilenvelikov
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-13T11:08:55Z

Review comments:
diff_hunk: @@ -174,10 +180,10 @@ PluginRegistry.add('guide.step', [
                 content: options.content || 'guide.step_plugin.guide-ended.content',
                 lastStep: true,
                 show: (guide) => () => {
-                    guide.options.confirmCancel = false;
+                    // guide.options.confirmCancel = false;
path: src/js/angular/guides/steps/core/plugin.js
user: login: svilenvelikov
body: Document why this is commented out or delete it
created_at: 2025-03-12T11:41:59Z
author_association: COLLABORATOR
diff_hunk: @@ -236,12 +236,28 @@ function GuidesService(
         if (angular.isDefined(startStepId)) {
             ShepherdService.resumeGuide(guide.guideId, stepsDescriptions, startStepId);
         } else {
-            ShepherdService.startGuide(guide.guideId, stepsDescriptions, startStepId);
+            ShepherdService.startGuide(guide.guideId, stepsDescriptions, startStepId, isAutoStarted);
         }
 
         $rootScope.$broadcast('guideStarted');
     };
 
+    this.autoStartGuide = (autoStartGuideId) => {
path: src/js/angular/guides/guides.service.js
user: login: svilenvelikov
body: Add normalization or check for this argument because right now you rely on the guideId and the request parameter to be of the same type which is string, but the guideId can be also a number and then the logic below might break.
created_at: 2025-03-12T11:45:36Z
author_association: COLLABORATOR
diff_hunk: @@ -179,15 +207,65 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
         return new Shepherd.Tour({
             name: guideId,
             useModalOverlay: true,
-            confirmCancel: true,
-            confirmCancelMessage: $translate.instant('guide.confirm.cancel.message'),
+            confirmCancel: this._confirmGuideCancel,
             defaultStepOptions: {
                 classes: 'shadow-md bg-purple-dark',
                 scrollTo: true
             }
         });
     };
 
+    this._confirmGuideCancel = () => {
+        const guide = Shepherd.activeTour;
+        const currentStep = guide.getCurrentStep()
+
+        if (currentStep.options.isLastStep) {
+            this._completeGuide(guide);
+            return;
+        }
+
+        guide.hide();
+
+        return ModalService.openCustomModal({
+            templateUrl: 'js/angular/guides/templates/modal-confirm-exit.html',
path: src/js/angular/guides/tour-lib-services/shepherd.service.js
user: login: svilenvelikov
body: For some reason when I try to stop the guide via the [x] button I get this error and the guide remains active
![image](https://github.com/user-attachments/assets/18075a38-e44b-4a57-8367-cebb5dc563e1)

created_at: 2025-03-12T11:49:29Z
author_association: COLLABORATOR
diff_hunk: @@ -174,10 +180,10 @@ PluginRegistry.add('guide.step', [
                 content: options.content || 'guide.step_plugin.guide-ended.content',
                 lastStep: true,
                 show: (guide) => () => {
-                    guide.options.confirmCancel = false;
+                    // guide.options.confirmCancel = false;
path: src/js/angular/guides/steps/core/plugin.js
user: login: yordanalexandrov
body: Forgot to remove
created_at: 2025-03-12T13:04:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,15 +207,65 @@ function ShepherdService($location, $translate, LocalStorageAdapter, $route, $in
         return new Shepherd.Tour({
             name: guideId,
             useModalOverlay: true,
-            confirmCancel: true,
-            confirmCancelMessage: $translate.instant('guide.confirm.cancel.message'),
+            confirmCancel: this._confirmGuideCancel,
             defaultStepOptions: {
                 classes: 'shadow-md bg-purple-dark',
                 scrollTo: true
             }
         });
     };
 
+    this._confirmGuideCancel = () => {
+        const guide = Shepherd.activeTour;
+        const currentStep = guide.getCurrentStep()
+
+        if (currentStep.options.isLastStep) {
+            this._completeGuide(guide);
+            return;
+        }
+
+        guide.hide();
+
+        return ModalService.openCustomModal({
+            templateUrl: 'js/angular/guides/templates/modal-confirm-exit.html',
path: src/js/angular/guides/tour-lib-services/shepherd.service.js
user: login: yordanalexandrov
body: Could it be that you did not rebuid the app when you switched the workspace?

The tamplate must be coppied by webpack, so a rebuild is needed.
created_at: 2025-03-12T13:06:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -236,12 +236,28 @@ function GuidesService(
         if (angular.isDefined(startStepId)) {
             ShepherdService.resumeGuide(guide.guideId, stepsDescriptions, startStepId);
         } else {
-            ShepherdService.startGuide(guide.guideId, stepsDescriptions, startStepId);
+            ShepherdService.startGuide(guide.guideId, stepsDescriptions, startStepId, isAutoStarted);
         }
 
         $rootScope.$broadcast('guideStarted');
     };
 
+    this.autoStartGuide = (autoStartGuideId) => {
path: src/js/angular/guides/guides.service.js
user: login: yordanalexandrov
body: Added check and documentation
created_at: 2025-03-13T09:26:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -236,12 +236,28 @@ function GuidesService(
         if (angular.isDefined(startStepId)) {
             ShepherdService.resumeGuide(guide.guideId, stepsDescriptions, startStepId);
         } else {
-            ShepherdService.startGuide(guide.guideId, stepsDescriptions, startStepId);
+            ShepherdService.startGuide(guide.guideId, stepsDescriptions, startStepId, isAutoStarted);
         }
 
         $rootScope.$broadcast('guideStarted');
     };
 
+    this.autoStartGuide = (autoStartGuideId) => {
path: src/js/angular/guides/guides.service.js
user: login: yordanalexandrov
body: Added check and conversion to string if `autoStartGuideId` is number.

Also added explicit conversion of `guide.guideId` to string as it is used as a further in the services.
created_at: 2025-03-13T10:27:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-13T18:08:11Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=Ontotext-AD_graphdb-workbench&pullRequest=1880) **Quality Gate failed for 'graphdb-workbench'**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Ontotext-AD_graphdb-workbench&pullRequest=1880&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Ontotext-AD_graphdb-workbench&pullRequest=1880)


author_association: NONE