core/templates/pages/exploration-player-page/learner-experience/conversation-skin.component.ts

Summary

Maintainability
F
1 wk
Test Coverage
// Copyright 2021 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Component for the conversation skin.
 */

import {Subscription} from 'rxjs';
import {StateCard} from 'domain/state_card/state-card.model';
import {ServicesConstants} from 'services/services.constants';
import {ChangeDetectorRef, Component, Input} from '@angular/core';
import {downgradeComponent} from '@angular/upgrade/static';
import {WindowRef} from 'services/contextual/window-ref.service';
import {AlertsService} from 'services/alerts.service';
import {AudioPlayerService} from 'services/audio-player.service';
import {AudioTranslationLanguageService} from '../services/audio-translation-language.service';
import {AutogeneratedAudioPlayerService} from 'services/autogenerated-audio-player.service';
import {ConceptCardBackendApiService} from 'domain/skill/concept-card-backend-api.service';
import {ContextService} from 'services/context.service';
import {CurrentInteractionService} from '../services/current-interaction.service';
import {ExplorationEngineService} from '../services/exploration-engine.service';
import {ExplorationPlayerStateService} from '../services/exploration-player-state.service';
import {ExplorationRecommendationsService} from '../services/exploration-recommendations.service';
import {FatigueDetectionService} from '../services/fatigue-detection.service';
import {FocusManagerService} from 'services/stateful/focus-manager.service';
import {GuestCollectionProgressService} from 'domain/collection/guest-collection-progress.service';
import {HintsAndSolutionManagerService} from '../services/hints-and-solution-manager.service';
import {
  I18nLanguageCodeService,
  TranslationKeyType,
} from 'services/i18n-language-code.service';
import {ImagePreloaderService} from '../services/image-preloader.service';
import {LearnerAnswerInfoService} from '../services/learner-answer-info.service';
import {LearnerParamsService} from '../services/learner-params.service';
import {LoaderService} from 'services/loader.service';
import {MessengerService} from 'services/messenger.service';
import {NumberAttemptsService} from '../services/number-attempts.service';
import {PlayerPositionService} from '../services/player-position.service';
import {PlayerTranscriptService} from '../services/player-transcript.service';
import {QuestionPlayerEngineService} from '../services/question-player-engine.service';
import {ReadOnlyCollectionBackendApiService} from 'domain/collection/read-only-collection-backend-api.service';
import {RefresherExplorationConfirmationModalService} from '../services/refresher-exploration-confirmation-modal.service';
import {SiteAnalyticsService} from 'services/site-analytics.service';
import {StatsReportingService} from '../services/stats-reporting.service';
import {StoryViewerBackendApiService} from 'domain/story_viewer/story-viewer-backend-api.service';
import {UrlService} from 'services/contextual/url.service';
import {UserService} from 'services/user.service';
import {LocalStorageService} from 'services/local-storage.service';
import {WindowDimensionsService} from 'services/contextual/window-dimensions.service';
import {QuestionPlayerStateService} from 'components/question-directives/question-player/services/question-player-state.service';
import {State} from 'domain/state/StateObjectFactory';
import {InteractionRulesService} from '../services/answer-classification.service';
import INTERACTION_SPECS from 'interactions/interaction_specs.json';
import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service';
import {ExplorationPlayerConstants} from '../exploration-player-page.constants';
import {AppConstants} from 'app.constants';
import {TopicViewerDomainConstants} from 'domain/topic_viewer/topic-viewer-domain.constants';
import {StoryViewerDomainConstants} from 'domain/story_viewer/story-viewer-domain.constants';
import {ConceptCard} from 'domain/skill/concept-card.model';
import {CollectionPlayerBackendApiService} from 'pages/collection-player-page/services/collection-player-backend-api.service';
import {ExplorationSummaryBackendApiService} from 'domain/summary/exploration-summary-backend-api.service';
import {LearnerExplorationSummary} from 'domain/summary/learner-exploration-summary.model';
import {EditableExplorationBackendApiService} from 'domain/exploration/editable-exploration-backend-api.service';
import {ReadOnlyExplorationBackendApiService} from 'domain/exploration/read-only-exploration-backend-api.service';
import {StateObjectsBackendDict} from 'domain/exploration/StatesObjectFactory';
import {PlatformFeatureService} from 'services/platform-feature.service';
import {LearnerDashboardBackendApiService} from 'domain/learner_dashboard/learner-dashboard-backend-api.service';
import {ConversationFlowService} from '../services/conversation-flow.service';

import './conversation-skin.component.css';
import {ConceptCardManagerService} from '../services/concept-card-manager.service';
import {TranslateService} from '@ngx-translate/core';
import {Solution} from 'domain/exploration/SolutionObjectFactory';

// Note: This file should be assumed to be in an IIFE, and the constants below
// should only be used within this file.
const TIME_FADEOUT_MSEC = 100;
const TIME_HEIGHT_CHANGE_MSEC = 500;
const TIME_FADEIN_MSEC = 100;
const TIME_NUM_CARDS_CHANGE_MSEC = 500;

@Component({
  selector: 'oppia-conversation-skin',
  templateUrl: './conversation-skin.component.html',
  styleUrls: ['./conversation-skin.component.css'],
})
export class ConversationSkinComponent {
  @Input() questionPlayerConfig;
  @Input() diagnosticTestTopicTrackerModel;
  directiveSubscriptions = new Subscription();

  TIME_PADDING_MSEC = 250;
  TIME_SCROLL_MSEC = 600;
  MIN_CARD_LOADING_DELAY_MSEC = 950;

  hasInteractedAtLeastOnce: boolean = false;
  _nextFocusLabel = null;
  _editorPreviewMode;
  explorationActuallyStarted: boolean = false;

  CONTINUE_BUTTON_FOCUS_LABEL =
    ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL;

  isLoggedIn: boolean;
  storyNodeIdToAdd: string;
  inStoryMode: boolean = false;
  collectionId: string;
  collectionTitle: string;
  answerIsBeingProcessed: boolean = false;
  explorationId: string;
  isInPreviewMode: boolean;
  isIframed: boolean;
  hasFullyLoaded = false;
  recommendedExplorationSummaries = [];
  answerIsCorrect = false;
  nextCard;
  nextCardIfStuck: StateCard | null;
  alertMessage = {};
  pendingCardWasSeenBefore: boolean = false;
  OPPIA_AVATAR_IMAGE_URL: string;
  displayedCard: StateCard;
  upcomingInlineInteractionHtml;
  responseTimeout: NodeJS.Timeout | null = null;
  DEFAULT_TWITTER_SHARE_MESSAGE_PLAYER =
    AppConstants.DEFAULT_TWITTER_SHARE_MESSAGE_EDITOR;

  // If the exploration is iframed, send data to its parent about
  // its height so that the parent can be resized as necessary.
  lastRequestedHeight: number = 0;
  lastRequestedScroll: boolean = false;
  startCardChangeAnimation: boolean;
  collectionSummary;
  redirectToRefresherExplorationConfirmed;
  isAnimatingToTwoCards: boolean;
  isAnimatingToOneCard: boolean;
  isRefresherExploration: boolean;
  parentExplorationIds: string[];
  conceptCard: ConceptCard;
  questionSessionCompleted: boolean;
  moveToExploration: boolean;
  upcomingInteractionInstructions;
  visitedStateNames: string[] = [];
  completedStateNames: string[] = [];
  prevSessionStatesProgress: string[] = [];
  mostRecentlyReachedCheckpoint: string;
  numberOfIncorrectSubmissions: number = 0;
  showProgressClearanceMessage: boolean = false;
  alertMessageTimeout = 6000;
  // 'completedChaptersCount' is fetched via a HTTP request.
  // Until the response is received, it remains undefined.
  completedChaptersCount: number | undefined;
  chapterIsCompletedForTheFirstTime: boolean = false;
  pidInUrl: string;
  submitButtonIsDisabled = true;
  solutionForState: Solution | null = null;
  isLearnerReallyStuck: boolean = false;
  continueToReviseStateButtonIsVisible: boolean = false;
  showInteraction: boolean = true;

  // The fields are used to customize the component for the diagnostic player,
  // question player, and exploration player page.
  feedbackIsEnabled: boolean = true;
  learnerCanOnlyAttemptQuestionOnce: boolean = false;
  inputOutputHistoryIsShown: boolean = true;
  navigationThroughCardHistoryIsEnabled: boolean = true;
  checkpointCelebrationModalIsEnabled: boolean = true;
  skipButtonIsShown: boolean = false;

  constructor(
    private windowRef: WindowRef,
    private alertsService: AlertsService,
    private audioPlayerService: AudioPlayerService,
    private audioTranslationLanguageService: AudioTranslationLanguageService,
    private autogeneratedAudioPlayerService: AutogeneratedAudioPlayerService,
    private changeDetectorRef: ChangeDetectorRef,
    private collectionPlayerBackendApiService: CollectionPlayerBackendApiService,
    private conceptCardBackendApiService: ConceptCardBackendApiService,
    private contextService: ContextService,
    private currentInteractionService: CurrentInteractionService,
    private explorationEngineService: ExplorationEngineService,
    private explorationPlayerStateService: ExplorationPlayerStateService,
    private explorationRecommendationsService: ExplorationRecommendationsService,
    private explorationSummaryBackendApiService: ExplorationSummaryBackendApiService,
    private fatigueDetectionService: FatigueDetectionService,
    private focusManagerService: FocusManagerService,
    private guestCollectionProgressService: GuestCollectionProgressService,
    private hintsAndSolutionManagerService: HintsAndSolutionManagerService,
    private conceptCardManagerService: ConceptCardManagerService,
    private i18nLanguageCodeService: I18nLanguageCodeService,
    private imagePreloaderService: ImagePreloaderService,
    private learnerAnswerInfoService: LearnerAnswerInfoService,
    private learnerParamsService: LearnerParamsService,
    private loaderService: LoaderService,
    private messengerService: MessengerService,
    private localStorageService: LocalStorageService,
    private numberAttemptsService: NumberAttemptsService,
    private playerPositionService: PlayerPositionService,
    private playerTranscriptService: PlayerTranscriptService,
    private questionPlayerEngineService: QuestionPlayerEngineService,
    private questionPlayerStateService: QuestionPlayerStateService,
    private readOnlyCollectionBackendApiService: ReadOnlyCollectionBackendApiService,
    private refresherExplorationConfirmationModalService: RefresherExplorationConfirmationModalService,
    private siteAnalyticsService: SiteAnalyticsService,
    private statsReportingService: StatsReportingService,
    private storyViewerBackendApiService: StoryViewerBackendApiService,
    private urlInterpolationService: UrlInterpolationService,
    private urlService: UrlService,
    private userService: UserService,
    private windowDimensionsService: WindowDimensionsService,
    private editableExplorationBackendApiService: EditableExplorationBackendApiService,
    private readOnlyExplorationBackendApiService: ReadOnlyExplorationBackendApiService,
    private platformFeatureService: PlatformFeatureService,
    private translateService: TranslateService,
    private learnerDashboardBackendApiService: LearnerDashboardBackendApiService,
    private conversationFlowService: ConversationFlowService
  ) {}

  adjustPageHeightOnresize(): void {
    this.windowRef.nativeWindow.onresize = () => {
      this.adjustPageHeight(false, null);
    };
  }

  ngOnInit(): void {
    this._editorPreviewMode = this.contextService.isInExplorationEditorPage();

    this.collectionId = this.urlService.getCollectionIdFromExplorationUrl();
    this.pidInUrl = this.urlService.getPidFromUrl();

    if (this.collectionId) {
      this.readOnlyCollectionBackendApiService
        .loadCollectionAsync(this.collectionId)
        .then(collection => {
          this.collectionTitle = collection.getTitle();
        });
    } else {
      this.collectionTitle = null;
    }

    if (this.diagnosticTestTopicTrackerModel) {
      this.feedbackIsEnabled = false;
      this.learnerCanOnlyAttemptQuestionOnce = true;
      this.inputOutputHistoryIsShown = false;
      this.navigationThroughCardHistoryIsEnabled = false;
      this.checkpointCelebrationModalIsEnabled = false;
      this.skipButtonIsShown = true;
    }

    if (!this.contextService.isInExplorationPlayerPage()) {
      this.checkpointCelebrationModalIsEnabled = false;
    }

    this.explorationId = this.explorationEngineService.getExplorationId();
    this.isInPreviewMode = this.explorationEngineService.isInPreviewMode();
    this.isIframed = this.urlService.isIframed();
    this.loaderService.showLoadingScreen('Loading');

    this.OPPIA_AVATAR_IMAGE_URL =
      this.urlInterpolationService.getStaticImageUrl(
        '/avatar/oppia_avatar_100px.svg'
      );

    if (this.explorationPlayerStateService.isInQuestionPlayerMode()) {
      this.directiveSubscriptions.add(
        this.hintsAndSolutionManagerService.onHintConsumed.subscribe(() => {
          this.questionPlayerStateService.hintUsed(
            this.questionPlayerEngineService.getCurrentQuestion()
          );
        })
      );

      this.directiveSubscriptions.add(
        this.hintsAndSolutionManagerService.onSolutionViewedEventEmitter.subscribe(
          () => {
            this.questionPlayerStateService.solutionViewed(
              this.questionPlayerEngineService.getCurrentQuestion()
            );
          }
        )
      );
    }

    this.directiveSubscriptions.add(
      this.explorationPlayerStateService.onShowProgressModal.subscribe(() => {
        this.hasFullyLoaded = true;
      })
    );

    this.directiveSubscriptions.add(
      this.playerPositionService.onNewCardOpened.subscribe(
        (newCard: StateCard) => {
          this.solutionForState = newCard.getSolution();
          this.numberOfIncorrectSubmissions = 0;
          this.nextCardIfStuck = null;
          this.continueToReviseStateButtonIsVisible = false;
          this.triggerIfLearnerStuckAction();
        }
      )
    );

    this.directiveSubscriptions.add(
      this.hintsAndSolutionManagerService.onLearnerReallyStuck.subscribe(() => {
        this.triggerIfLearnerStuckActionDirectly();
      })
    );

    this.directiveSubscriptions.add(
      this.hintsAndSolutionManagerService.onHintsExhausted.subscribe(() => {
        this.triggerIfLearnerStuckAction();
      })
    );

    this.directiveSubscriptions.add(
      this.conceptCardManagerService.onLearnerGetsReallyStuck.subscribe(() => {
        this.isLearnerReallyStuck = true;
        this.triggerIfLearnerStuckActionDirectly();
      })
    );

    this.directiveSubscriptions.add(
      this.explorationPlayerStateService.onPlayerStateChange.subscribe(
        newStateName => {
          if (!newStateName) {
            return;
          }
          // To restart the preloader for the new state if required.
          if (!this._editorPreviewMode) {
            this.imagePreloaderService.onStateChange(newStateName);
          }
          // Ensure the transition to a terminal state properly logs
          // the end of the exploration.
          if (!this._editorPreviewMode && this.nextCard.isTerminal()) {
            const currentEngineService =
              this.explorationPlayerStateService.getCurrentEngineService();
            this.statsReportingService.recordExplorationCompleted(
              newStateName,
              this.learnerParamsService.getAllParams(),
              String(
                this.completedChaptersCount && this.completedChaptersCount + 1
              ),
              String(this.playerTranscriptService.getNumCards()),
              currentEngineService.getLanguageCode()
            );

            // If the user is a guest, has completed this exploration
            // within the context of a collection, and the collection is
            // allowlisted, record their temporary progress.

            if (
              this.doesCollectionAllowsGuestProgress(this.collectionId) &&
              !this.isLoggedIn
            ) {
              this.guestCollectionProgressService.recordExplorationCompletedInCollection(
                this.collectionId,
                this.explorationId
              );
            }

            // For single state explorations, when the exploration
            // reachesthe terminal state and explorationActuallyStarted
            // is false, record exploration actual start event.
            if (!this.explorationActuallyStarted) {
              this.statsReportingService.recordExplorationActuallyStarted(
                newStateName
              );
              this.explorationActuallyStarted = true;
            }
          }
        }
      )
    );

    // Moved the following code to then section as isLoggedIn
    // variable needs to be defined before the following code is executed.
    this.userService.getUserInfoAsync().then(async userInfo => {
      this.isLoggedIn = userInfo.isLoggedIn();

      this.windowRef.nativeWindow.addEventListener('beforeunload', e => {
        if (this.redirectToRefresherExplorationConfirmed) {
          return;
        }
        if (
          this.hasInteractedAtLeastOnce &&
          !this.isInPreviewMode &&
          !this.displayedCard.isTerminal() &&
          !this.explorationPlayerStateService.isInQuestionMode()
        ) {
          this.statsReportingService.recordMaybeLeaveEvent(
            this.playerTranscriptService.getLastStateName(),
            this.learnerParamsService.getAllParams()
          );

          let confirmationMessage =
            'Please save your progress before navigating away from the' +
            ' page; else, you will lose your exploration progress.';
          (e || this.windowRef.nativeWindow.event).returnValue =
            confirmationMessage;
          return confirmationMessage;
        }
      });

      let pid =
        this.localStorageService.getUniqueProgressIdOfLoggedOutLearner();
      if (pid && this.isLoggedIn) {
        await this.editableExplorationBackendApiService.changeLoggedOutProgressToLoggedInProgressAsync(
          this.explorationId,
          pid
        );
        this.localStorageService.removeUniqueProgressIdOfLoggedOutLearner();
      }

      this.adjustPageHeightOnresize();

      this.currentInteractionService.setOnSubmitFn(
        this.submitAnswer.bind(this)
      );
      this.startCardChangeAnimation = false;
      this.initializePage();

      this.collectionSummary = null;

      if (this.collectionId) {
        this.collectionPlayerBackendApiService
          .fetchCollectionSummariesAsync(this.collectionId)
          .then(
            response => {
              this.collectionSummary = response.summaries[0];
            },
            () => {
              this.alertsService.addWarning(
                'There was an error while fetching the collection ' + 'summary.'
              );
            }
          );
      }

      this.fetchCompletedChaptersCount();

      // We do not save checkpoints progress for iframes.
      if (
        !this.isIframed &&
        !this._editorPreviewMode &&
        !this.explorationPlayerStateService.isInQuestionPlayerMode()
      ) {
        // For the first state which is always a checkpoint.
        let firstStateName: string;
        let expVersion: number;
        this.readOnlyExplorationBackendApiService
          .loadLatestExplorationAsync(this.explorationId, this.pidInUrl)
          .then(response => {
            expVersion = response.version;
            firstStateName = response.exploration.init_state_name;
            this.mostRecentlyReachedCheckpoint =
              response.most_recently_reached_checkpoint_state_name;
            // If the exploration is freshly started, mark the first state
            // as the most recently reached checkpoint.
            if (!this.mostRecentlyReachedCheckpoint && this.isLoggedIn) {
              this.editableExplorationBackendApiService.recordMostRecentlyReachedCheckpointAsync(
                this.explorationId,
                expVersion,
                firstStateName,
                true
              );
            }
            this.explorationPlayerStateService.setLastCompletedCheckpoint(
              firstStateName
            );
          });
        this.visitedStateNames.push(firstStateName);
      }
    });
  }

  doesCollectionAllowsGuestProgress(collectionId: string | never): boolean {
    let allowedCollectionIds =
      AppConstants.ALLOWED_COLLECTION_IDS_FOR_SAVING_GUEST_PROGRESS;
    return (
      (allowedCollectionIds as readonly []).indexOf(collectionId as never) !==
      -1
    );
  }

  isSubmitButtonDisabled(): boolean {
    let currentIndex = this.playerPositionService.getDisplayedCardIndex();
    // This check is added because it was observed that when returning
    // to current card after navigating through previous cards, using
    // the arrows, the Submit button was sometimes falsely disabled.
    // Also, since a learner's answers would always be in the current
    // card, this additional check doesn't interfere with its normal
    // working.
    if (!this.playerTranscriptService.isLastCard(currentIndex)) {
      return false;
    }
    return this.currentInteractionService.isSubmitButtonDisabled();
  }

  ngAfterViewChecked(): void {
    let submitButtonIsDisabled = this.isSubmitButtonDisabled();
    if (submitButtonIsDisabled !== this.submitButtonIsDisabled) {
      this.submitButtonIsDisabled = submitButtonIsDisabled;
      this.changeDetectorRef.detectChanges();
    }
  }

  changeCard(index: number): void {
    this.playerPositionService.recordNavigationButtonClick();
    this.playerPositionService.setDisplayedCardIndex(index);
    this.explorationEngineService.onUpdateActiveStateIfInEditor.emit(
      this.playerPositionService.getCurrentStateName()
    );
    this.playerPositionService.changeCurrentQuestion(index);
  }

  ngOnDestroy(): void {
    this.directiveSubscriptions.unsubscribe();
  }

  alwaysAskLearnerForAnswerDetails(): boolean {
    return this.explorationEngineService.getAlwaysAskLearnerForAnswerDetails();
  }

  getCanAskLearnerForAnswerInfo(): boolean {
    return this.learnerAnswerInfoService.getCanAskLearnerForAnswerInfo();
  }

  fetchCompletedChaptersCount(): void {
    if (this.isLoggedIn) {
      this.learnerDashboardBackendApiService
        .fetchLearnerCompletedChaptersCountDataAsync()
        .then(data => {
          this.completedChaptersCount = data.completedChaptersCount;
        });
    }
  }

  initLearnerAnswerInfoService(
    entityId: string,
    state: State,
    answer: string,
    interactionRulesService: InteractionRulesService,
    alwaysAskLearnerForAnswerInfo: boolean
  ): void {
    this.learnerAnswerInfoService.initLearnerAnswerInfoService(
      entityId,
      state,
      answer,
      interactionRulesService,
      alwaysAskLearnerForAnswerInfo
    );
  }

  isCorrectnessFooterEnabled(): boolean {
    return (
      this.answerIsCorrect &&
      this.playerPositionService.hasLearnerJustSubmittedAnAnswer()
    );
  }

  isLearnAgainButton(): boolean {
    let conceptCardIsBeingShown =
      this.displayedCard.getStateName() === null &&
      !this.explorationPlayerStateService.isInQuestionMode();
    if (conceptCardIsBeingShown) {
      return false;
    }
    let interaction = this.displayedCard.getInteraction();

    if (!interaction.id) {
      // An editor might also try to view preview tab without adding
      // interaction to concept card.
      return false;
    }

    if (INTERACTION_SPECS[interaction.id].is_linear) {
      return false;
    }
    return this.pendingCardWasSeenBefore && !this.answerIsCorrect;
  }

  private _getRandomSuffix(): string {
    // This is a bit of a hack. When a refresh to a $scope variable
    // happens,
    // AngularJS compares the new value of the variable to its previous
    // value. If they are the same, then the variable is not updated.
    // Appending a random suffix makes the new value different from the
    // previous one, and thus indirectly forces a refresh.
    let randomSuffix = '';
    let N = Math.round(Math.random() * 1000);
    for (let i = 0; i < N; i++) {
      randomSuffix += ' ';
    }
    return randomSuffix;
  }

  getStaticImageUrl(imagePath: string): string {
    return this.urlInterpolationService.getStaticImageUrl(imagePath);
  }

  getContentFocusLabel(index: number): string {
    return ExplorationPlayerConstants.CONTENT_FOCUS_LABEL_PREFIX + index;
  }

  adjustPageHeight(scroll: boolean, callback: () => void): void {
    setTimeout(() => {
      let newHeight = document.body.scrollHeight;
      if (
        Math.abs(this.lastRequestedHeight - newHeight) > 50.5 ||
        (scroll && !this.lastRequestedScroll)
      ) {
        // Sometimes setting iframe height to the exact content height
        // still produces scrollbar, so adding 50 extra px.
        newHeight += 50;
        this.messengerService.sendMessage(
          ServicesConstants.MESSENGER_PAYLOAD.HEIGHT_CHANGE,
          {
            height: newHeight,
            scroll: scroll,
          }
        );
        this.lastRequestedHeight = newHeight;
        this.lastRequestedScroll = scroll;
      }

      if (callback) {
        callback();
      }
    }, 100);
  }

  getExplorationLink(): string {
    if (
      this.recommendedExplorationSummaries &&
      this.recommendedExplorationSummaries[0]
    ) {
      if (!this.recommendedExplorationSummaries[0].id) {
        return '#';
      } else {
        let result = '/explore/' + this.recommendedExplorationSummaries[0].id;
        let urlParams = this.urlService.getUrlParams();
        let parentExplorationIds =
          this.recommendedExplorationSummaries[0].parentExplorationIds;

        let collectionIdToAdd = this.collectionId;
        let storyUrlFragmentToAdd = null;
        let topicUrlFragment = null;
        let classroomUrlFragment = null;
        // Replace the collection ID with the one in the URL if it
        // exists in urlParams.
        if (parentExplorationIds && urlParams.hasOwnProperty('collection_id')) {
          collectionIdToAdd = urlParams.collection_id;
        } else if (
          this.urlService.getPathname().match(/\/story\/(\w|-){12}/g) &&
          this.recommendedExplorationSummaries[0].nextNodeId
        ) {
          storyUrlFragmentToAdd =
            this.urlService.getStoryUrlFragmentFromLearnerUrl();
          topicUrlFragment =
            this.urlService.getTopicUrlFragmentFromLearnerUrl();
          classroomUrlFragment =
            this.urlService.getClassroomUrlFragmentFromLearnerUrl();
        } else if (
          urlParams.hasOwnProperty('story_url_fragment') &&
          urlParams.hasOwnProperty('node_id') &&
          urlParams.hasOwnProperty('topic_url_fragment') &&
          urlParams.hasOwnProperty('classroom_url_fragment')
        ) {
          topicUrlFragment = urlParams.topic_url_fragment;
          classroomUrlFragment = urlParams.classroom_url_fragment;
          storyUrlFragmentToAdd = urlParams.story_url_fragment;
        }

        if (collectionIdToAdd) {
          result = this.urlService.addField(
            result,
            'collection_id',
            collectionIdToAdd
          );
        }
        if (parentExplorationIds) {
          for (let i = 0; i < parentExplorationIds.length - 1; i++) {
            result = this.urlService.addField(
              result,
              'parent',
              parentExplorationIds[i]
            );
          }
        }
        if (storyUrlFragmentToAdd && this.storyNodeIdToAdd) {
          result = this.urlService.addField(
            result,
            'topic_url_fragment',
            topicUrlFragment
          );
          result = this.urlService.addField(
            result,
            'classroom_url_fragment',
            classroomUrlFragment
          );
          result = this.urlService.addField(
            result,
            'story_url_fragment',
            storyUrlFragmentToAdd
          );
          result = this.urlService.addField(
            result,
            'node_id',
            this.storyNodeIdToAdd
          );
        }
        return result;
      }
    }
  }

  isEndChapterCelebrationFeatureEnabled(): boolean {
    return this.platformFeatureService.status.EndChapterCelebration.isEnabled;
  }

  reloadExploration(): void {
    this.windowRef.nativeWindow.location.reload();
  }

  isOnTerminalCard(): boolean {
    return this.displayedCard && this.displayedCard.isTerminal();
  }

  isCurrentSupplementalCardNonempty(): boolean {
    return (
      this.displayedCard &&
      this.conversationFlowService.isSupplementalCardNonempty(
        this.displayedCard
      )
    );
  }

  isSupplementalNavShown(): boolean {
    if (
      this.displayedCard.getStateName() === null &&
      !this.explorationPlayerStateService.isInQuestionMode()
    ) {
      return false;
    }
    let interaction = this.displayedCard.getInteraction();
    return (
      Boolean(interaction.id) &&
      INTERACTION_SPECS[interaction.id].show_generic_submit_button &&
      this.isCurrentCardAtEndOfTranscript()
    );
  }

  private _recordLeaveForRefresherExp(refresherExpId): void {
    if (!this._editorPreviewMode) {
      this.statsReportingService.recordLeaveForRefresherExp(
        this.playerPositionService.getCurrentStateName(),
        refresherExpId
      );
    }
  }

  private _navigateToMostRecentlyReachedCheckpoint() {
    let states: StateObjectsBackendDict;
    this.readOnlyExplorationBackendApiService
      .loadLatestExplorationAsync(this.explorationId, this.pidInUrl)
      .then(response => {
        states = response.exploration.states;
        this.mostRecentlyReachedCheckpoint =
          response.most_recently_reached_checkpoint_state_name;

        this.prevSessionStatesProgress =
          this.explorationEngineService.getShortestPathToState(
            states,
            this.mostRecentlyReachedCheckpoint
          );

        let indexToRedirectTo = 0;

        for (let i = 0; i < this.prevSessionStatesProgress.length; i++) {
          // Set state name of a previously completed state.
          let stateName = this.prevSessionStatesProgress[i];
          // Skip the card if it has already been added to transcript.
          if (
            !this.playerTranscriptService.hasEncounteredStateBefore(stateName)
          ) {
            let stateCard =
              this.explorationEngineService.getStateCardByName(stateName);
            this._addNewCard(stateCard);
          }

          if (this.mostRecentlyReachedCheckpoint === stateName) {
            break;
          }

          this.visitedStateNames.push(stateName);
          indexToRedirectTo += 1;
        }

        // Remove the last card from progress as it is not completed
        // yet and is only most recently reached.
        this.prevSessionStatesProgress.pop();

        if (indexToRedirectTo > 0) {
          setTimeout(() => {
            let alertInfoElement = document.querySelector(
              '.oppia-exploration-checkpoints-message'
            );

            // Remove the alert message after 6 sec.
            if (alertInfoElement) {
              alertInfoElement.remove();
            }
          }, this.alertMessageTimeout);
        }

        // Move to most recently reached checkpoint card.
        this.changeCard(indexToRedirectTo);
        this.playerPositionService.onLoadedMostRecentCheckpoint.emit();
      });
  }

  // Navigates to the currently-active card, and resets the
  // 'show previous responses' setting.
  private _navigateToDisplayedCard(): void {
    let index = this.playerPositionService.getDisplayedCardIndex();
    this.displayedCard = this.playerTranscriptService.getCard(index);

    if (
      index > 0 &&
      !this.isIframed &&
      !this._editorPreviewMode &&
      !this.explorationPlayerStateService.isInQuestionPlayerMode()
    ) {
      let currentState = this.explorationEngineService.getState();
      let currentStateName = currentState.name;
      if (
        currentState.cardIsCheckpoint &&
        !this.visitedStateNames.includes(currentStateName) &&
        !this.prevSessionStatesProgress.includes(currentStateName)
      ) {
        this.readOnlyExplorationBackendApiService
          .loadLatestExplorationAsync(this.explorationId)
          .then(response => {
            this.explorationPlayerStateService.setLastCompletedCheckpoint(
              currentStateName
            );
            this.editableExplorationBackendApiService.recordMostRecentlyReachedCheckpointAsync(
              this.explorationId,
              response.version,
              currentStateName,
              this.isLoggedIn,
              this.explorationPlayerStateService.getUniqueProgressUrlId()
            );
          });
        this.visitedStateNames.push(currentStateName);
      }
    }

    this.playerPositionService.onActiveCardChanged.emit();

    this.audioPlayerService.onAutoplayAudio.emit();
    /* A hash value is added to URL for scrolling to Oppia feedback
        when answer is submitted by user in mobile view. This hash value
        has to be reset each time a new card is loaded to prevent
        unwanted scrolling in the new card. */

    // $location.hash(null);

    // We must cancel the autogenerated audio player here, or else a
    // bug where the autogenerated audio player generates duplicate
    // utterances occurs.
    this.autogeneratedAudioPlayerService.cancel();
    if (
      this._nextFocusLabel &&
      this.playerTranscriptService.isLastCard(index)
    ) {
      this.focusManagerService.setFocusIfOnDesktop(this._nextFocusLabel);
    } else {
      this.focusManagerService.setFocusIfOnDesktop(
        this.getContentFocusLabel(index)
      );
    }
  }

  returnToExplorationAfterConceptCard(): void {
    this.playerTranscriptService.addPreviousCard();
    let numCards = this.playerTranscriptService.getNumCards();
    this.playerPositionService.setDisplayedCardIndex(numCards - 1);
  }

  isCurrentCardAtEndOfTranscript(): boolean {
    return this.playerTranscriptService.isLastCard(
      this.playerPositionService.getDisplayedCardIndex()
    );
  }

  private _addNewCard(newCard): void {
    this.conversationFlowService.addNewCard(newCard);

    let totalNumCards = this.playerTranscriptService.getNumCards();

    let previousSupplementalCardIsNonempty =
      totalNumCards > 1 &&
      this.conversationFlowService.isSupplementalCardNonempty(
        this.playerTranscriptService.getCard(totalNumCards - 2)
      );

    let nextSupplementalCardIsNonempty =
      this.conversationFlowService.isSupplementalCardNonempty(
        this.playerTranscriptService.getLastCard()
      );

    if (
      totalNumCards > 1 &&
      this.canWindowShowTwoCards() &&
      !previousSupplementalCardIsNonempty &&
      nextSupplementalCardIsNonempty
    ) {
      this.playerPositionService.setDisplayedCardIndex(totalNumCards - 1);
      this.animateToTwoCards(function () {});
    } else if (
      totalNumCards > 1 &&
      this.canWindowShowTwoCards() &&
      previousSupplementalCardIsNonempty &&
      !nextSupplementalCardIsNonempty
    ) {
      this.animateToOneCard(() => {
        this.playerPositionService.setDisplayedCardIndex(totalNumCards - 1);
      });
    } else {
      this.playerPositionService.setDisplayedCardIndex(totalNumCards - 1);
    }
    this.playerPositionService.changeCurrentQuestion(
      this.playerPositionService.getDisplayedCardIndex()
    );

    if (this.displayedCard && this.displayedCard.isTerminal()) {
      this.isRefresherExploration = false;
      this.parentExplorationIds =
        this.urlService.getQueryFieldValuesAsList('parent');
      let recommendedExplorationIds = [];
      let includeAutogeneratedRecommendations = false;

      if (this.parentExplorationIds.length > 0) {
        this.isRefresherExploration = true;
        let parentExplorationId =
          this.parentExplorationIds[this.parentExplorationIds.length - 1];
        recommendedExplorationIds.push(parentExplorationId);
      } else {
        recommendedExplorationIds =
          this.explorationEngineService.getAuthorRecommendedExpIdsByStateName(
            this.displayedCard.getStateName()
          );
        includeAutogeneratedRecommendations = true;
      }

      if (
        this.explorationPlayerStateService.isInStoryChapterMode() &&
        AppConstants.ENABLE_NEW_STRUCTURE_VIEWER_UPDATES
      ) {
        recommendedExplorationIds = [];
        includeAutogeneratedRecommendations = false;
        let topicUrlFragment =
          this.urlService.getUrlParams().topic_url_fragment;
        let classroomUrlFragment =
          this.urlService.getUrlParams().classroom_url_fragment;
        let storyUrlFragment =
          this.urlService.getUrlParams().story_url_fragment;
        let nodeId = this.urlService.getUrlParams().node_id;
        this.inStoryMode = true;
        this.storyViewerBackendApiService
          .fetchStoryDataAsync(
            topicUrlFragment,
            classroomUrlFragment,
            storyUrlFragment
          )
          .then(res => {
            let nextStoryNode: LearnerExplorationSummary[] = [];
            for (let i = 0; i < res.nodes.length; i++) {
              if (res.nodes[i].id === nodeId && i + 1 < res.nodes.length) {
                this.storyNodeIdToAdd = res.nodes[i].destinationNodeIds[0];
                nextStoryNode.push(res.nodes[i + 1].explorationSummary);
                break;
              }
            }
            this.recommendedExplorationSummaries = nextStoryNode;
          });
        if (this.isLoggedIn) {
          this.storyViewerBackendApiService
            .recordChapterCompletionAsync(
              topicUrlFragment,
              classroomUrlFragment,
              storyUrlFragment,
              nodeId
            )
            .then(returnObject => {
              if (returnObject.readyForReviewTest) {
                (
                  this.windowRef.nativeWindow as {location: string | Location}
                ).location = this.urlInterpolationService.interpolateUrl(
                  TopicViewerDomainConstants.REVIEW_TESTS_URL_TEMPLATE,
                  {
                    topic_url_fragment: topicUrlFragment,
                    classroom_url_fragment: classroomUrlFragment,
                    story_url_fragment: storyUrlFragment,
                  }
                );
              }
              this.learnerDashboardBackendApiService
                .fetchLearnerCompletedChaptersCountDataAsync()
                .then(responseData => {
                  let newCompletedChaptersCount =
                    responseData.completedChaptersCount;
                  if (
                    newCompletedChaptersCount !== this.completedChaptersCount
                  ) {
                    this.completedChaptersCount = newCompletedChaptersCount;
                    this.chapterIsCompletedForTheFirstTime = true;
                  }
                });
            });
        } else {
          let loginRedirectUrl = this.urlInterpolationService.interpolateUrl(
            StoryViewerDomainConstants.STORY_PROGRESS_URL_TEMPLATE,
            {
              topic_url_fragment: topicUrlFragment,
              classroom_url_fragment: classroomUrlFragment,
              story_url_fragment: storyUrlFragment,
              node_id: nodeId,
            }
          );
          this.userService.setReturnUrl(loginRedirectUrl);
        }
      } else {
        this.explorationRecommendationsService.getRecommendedSummaryDicts(
          recommendedExplorationIds,
          includeAutogeneratedRecommendations,
          summaries => {
            this.recommendedExplorationSummaries = summaries;
          }
        );
      }

      if (!this.showProgressClearanceMessage) {
        this.showProgressClearanceMessage = true;
        setTimeout(() => {
          let alertInfoElement = document.querySelector(
            '.oppia-exploration-checkpoints-message'
          );

          // Remove the alert message after 6 sec.
          if (alertInfoElement) {
            alertInfoElement.remove();
          }
        }, this.alertMessageTimeout);
      }
    }
  }

  triggerIfLearnerStuckAction(): void {
    if (this.responseTimeout) {
      clearTimeout(this.responseTimeout);
      this.responseTimeout = null;
    }
    this.responseTimeout = setTimeout(() => {
      if (this.nextCardIfStuck && this.nextCardIfStuck !== this.displayedCard) {
        // Let the learner know about the redirection to a state
        // for clearing concepts.
        this.playerTranscriptService.addNewResponseToExistingFeedback(
          this.translateService.instant(
            'I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE'
          )
        );
        // Enable visibility of ContinueToRevise button.
        this.continueToReviseStateButtonIsVisible = true;
      } else if (
        this.solutionForState !== null &&
        this.numberOfIncorrectSubmissions >=
          ExplorationPlayerConstants.MAX_INCORRECT_ANSWERS_BEFORE_RELEASING_SOLUTION
      ) {
        // Release solution if no separate state for addressing
        // the stuck learner exists and the solution exists.
        this.hintsAndSolutionManagerService.releaseSolution();
      }
    }, ExplorationPlayerConstants.WAIT_BEFORE_RESPONSE_FOR_STUCK_LEARNER_MSEC);
  }

  triggerIfLearnerStuckActionDirectly(): void {
    if (this.responseTimeout) {
      clearTimeout(this.responseTimeout);
      this.responseTimeout = null;
    }
    // Directly trigger action for the really stuck learner.
    if (this.nextCardIfStuck && this.nextCardIfStuck !== this.displayedCard) {
      this.playerTranscriptService.addNewResponseToExistingFeedback(
        this.translateService.instant('I18N_REDIRECTION_TO_STUCK_STATE_MESSAGE')
      );
      // Enable visibility of ContinueToRevise button.
      this.continueToReviseStateButtonIsVisible = true;
    } else if (
      this.solutionForState !== null &&
      this.numberOfIncorrectSubmissions >=
        ExplorationPlayerConstants.MAX_INCORRECT_ANSWERS_BEFORE_RELEASING_SOLUTION
    ) {
      // Release solution if it exists.
      this.hintsAndSolutionManagerService.releaseSolution();
    }
  }

  triggerRedirectionToStuckState(): void {
    // Redirect the learner.
    this.nextCard = this.nextCardIfStuck;
    this.showInteraction = false;
    this.showPendingCard();
  }

  showQuestionAreNotAvailable(): void {
    this.loaderService.hideLoadingScreen();
  }

  private _initializeDirectiveComponents(initialCard, focusLabel): void {
    this._addNewCard(initialCard);
    this.nextCard = initialCard;
    if (!this.explorationPlayerStateService.isInDiagnosticTestPlayerMode()) {
      this.explorationPlayerStateService.onPlayerStateChange.emit(
        this.nextCard.getStateName()
      );
    }

    // We do not store checkpoints progress for iframes hence we do not
    // need to consider redirecting the user to the most recently
    // reached checkpoint on exploration initial load in that case.
    if (
      !this.isIframed &&
      !this._editorPreviewMode &&
      !this.explorationPlayerStateService.isInQuestionPlayerMode()
    ) {
      // Navigate the learner to the most recently reached checkpoint state.
      this._navigateToMostRecentlyReachedCheckpoint();
    }
    this.hasFullyLoaded = true;

    this.focusManagerService.setFocusIfOnDesktop(focusLabel);
    this.loaderService.hideLoadingScreen();

    // If the exploration is embedded, use the url language code
    // as site language. If the url language code is not supported
    // as site language, English is used as default.
    let langCodes = AppConstants.SUPPORTED_SITE_LANGUAGES.map(language => {
      return language.id;
    }) as string[];
    if (this.isIframed) {
      let urlLanguageCode = this.urlService.getUrlParams().lang;
      if (urlLanguageCode && langCodes.indexOf(urlLanguageCode) !== -1) {
        this.i18nLanguageCodeService.setI18nLanguageCode(urlLanguageCode);
      } else {
        this.i18nLanguageCodeService.setI18nLanguageCode('en');
      }
    }
    this.adjustPageHeight(false, null);
    this.windowRef.nativeWindow.scrollTo(0, 0);

    // The timeout is needed in order to give the recipient of the
    // broadcast sufficient time to load.
    setTimeout(() => {
      this.playerPositionService.onNewCardOpened.emit(initialCard);
    });
  }

  skipCurrentQuestion(): void {
    this.explorationPlayerStateService.skipCurrentQuestion(nextCard => {
      this.nextCard = nextCard;
      this.showPendingCard();
    });
  }

  initializePage(): void {
    this.hasInteractedAtLeastOnce = false;
    this.recommendedExplorationSummaries = [];
    this.playerPositionService.init(this._navigateToDisplayedCard.bind(this));
    if (this.questionPlayerConfig) {
      this.explorationPlayerStateService.initializeQuestionPlayer(
        this.questionPlayerConfig,
        this._initializeDirectiveComponents.bind(this),
        this.showQuestionAreNotAvailable
      );
    } else if (this.diagnosticTestTopicTrackerModel) {
      this.explorationPlayerStateService.initializeDiagnosticPlayer(
        this.diagnosticTestTopicTrackerModel,
        this._initializeDirectiveComponents.bind(this)
      );
    } else {
      this.explorationPlayerStateService.initializePlayer(
        this._initializeDirectiveComponents.bind(this)
      );
    }
  }

  submitAnswer(
    answer: string,
    interactionRulesService: InteractionRulesService
  ): void {
    this.displayedCard.updateCurrentAnswer(null);

    // Safety check to prevent double submissions from occurring.
    if (
      this.answerIsBeingProcessed ||
      !this.isCurrentCardAtEndOfTranscript() ||
      this.displayedCard.isCompleted()
    ) {
      return;
    }

    if (!this.isInPreviewMode) {
      this.fatigueDetectionService.recordSubmissionTimestamp();
      if (this.fatigueDetectionService.isSubmittingTooFast()) {
        this.fatigueDetectionService.displayTakeBreakMessage();
        this.explorationPlayerStateService.onOppiaFeedbackAvailable.emit();
        return;
      }
    }

    if (
      !this.isInPreviewMode &&
      !this.explorationPlayerStateService.isPresentingIsolatedQuestions() &&
      AppConstants.ENABLE_SOLICIT_ANSWER_DETAILS_FEATURE
    ) {
      this.initLearnerAnswerInfoService(
        this.explorationId,
        this.explorationEngineService.getState(),
        answer,
        interactionRulesService,
        this.alwaysAskLearnerForAnswerDetails()
      );
    }

    this.numberAttemptsService.submitAttempt();

    this.answerIsBeingProcessed = true;
    this.hasInteractedAtLeastOnce = true;

    this.playerTranscriptService.addNewInput(answer, false);

    if (this.getCanAskLearnerForAnswerInfo()) {
      setTimeout(() => {
        this.playerTranscriptService.addNewResponse(
          this.learnerAnswerInfoService.getSolicitAnswerDetailsQuestion()
        );
        this.answerIsBeingProcessed = false;
        this.playerPositionService.onHelpCardAvailable.emit({
          helpCardHtml:
            this.learnerAnswerInfoService.getSolicitAnswerDetailsQuestion(),
          hasContinueButton: false,
        });
      }, 100);
      return;
    }

    let timeAtServerCall = new Date().getTime();
    this.playerPositionService.recordAnswerSubmission();
    let currentEngineService =
      this.explorationPlayerStateService.getCurrentEngineService();
    this.answerIsCorrect = currentEngineService.submitAnswer(
      answer,
      interactionRulesService,
      (
        nextCard,
        refreshInteraction,
        feedbackHtml,
        feedbackAudioTranslations,
        refresherExplorationId,
        missingPrerequisiteSkillId,
        remainOnCurrentCard,
        taggedSkillMisconceptionId,
        wasOldStateInitial,
        isFirstHit,
        isFinalQuestion,
        nextCardIfReallyStuck,
        focusLabel
      ) => {
        this.nextCard = nextCard;
        this.nextCardIfStuck = nextCardIfReallyStuck;
        if (
          !this._editorPreviewMode &&
          !this.explorationPlayerStateService.isPresentingIsolatedQuestions()
        ) {
          let oldStateName = this.playerPositionService.getCurrentStateName();
          if (!remainOnCurrentCard) {
            this.statsReportingService.recordStateTransition(
              oldStateName,
              nextCard.getStateName(),
              answer,
              this.learnerParamsService.getAllParams(),
              isFirstHit,
              String(
                this.completedChaptersCount && this.completedChaptersCount + 1
              ),
              String(this.playerTranscriptService.getNumCards()),
              currentEngineService.getLanguageCode()
            );

            this.statsReportingService.recordStateCompleted(oldStateName);
          }
          if (nextCard.isTerminal()) {
            this.statsReportingService.recordStateCompleted(
              nextCard.getStateName()
            );
          }
          if (wasOldStateInitial && !this.explorationActuallyStarted) {
            this.statsReportingService.recordExplorationActuallyStarted(
              oldStateName
            );
            this.explorationActuallyStarted = true;
          }
        }

        if (
          !this.explorationPlayerStateService.isPresentingIsolatedQuestions()
        ) {
          this.explorationPlayerStateService.onPlayerStateChange.emit(
            nextCard.getStateName()
          );
        } else if (
          this.explorationPlayerStateService.isInQuestionPlayerMode()
        ) {
          this.questionPlayerStateService.answerSubmitted(
            this.questionPlayerEngineService.getCurrentQuestion(),
            !remainOnCurrentCard,
            taggedSkillMisconceptionId
          );
        }

        let millisecsLeftToWait: number;
        if (!this.displayedCard.isInteractionInline()) {
          // Do not wait if the interaction is supplemental -- there's
          // already a delay bringing in the help card.
          millisecsLeftToWait = 1.0;
        } else if (
          this.explorationPlayerStateService.isInDiagnosticTestPlayerMode()
        ) {
          // Do not wait if the player mode is the diagnostic test. Since no
          // feedback will be presented after attempting a question so delaying
          // is not required.
          millisecsLeftToWait = 1.0;
        } else {
          millisecsLeftToWait = Math.max(
            this.MIN_CARD_LOADING_DELAY_MSEC -
              (new Date().getTime() - timeAtServerCall),
            1.0
          );
        }

        setTimeout(() => {
          this.explorationPlayerStateService.onOppiaFeedbackAvailable.emit();

          this.audioPlayerService.onAutoplayAudio.emit({
            audioTranslations: feedbackAudioTranslations,
            html: feedbackHtml,
            componentName: AppConstants.COMPONENT_NAME_FEEDBACK,
          });

          if (remainOnCurrentCard) {
            this.giveFeedbackAndStayOnCurrentCard(
              feedbackHtml,
              missingPrerequisiteSkillId,
              refreshInteraction,
              refresherExplorationId
            );
          } else {
            this.moveToNewCard(feedbackHtml, isFinalQuestion, nextCard);
          }
          this.answerIsBeingProcessed = false;
        }, millisecsLeftToWait);
      }
    );
  }

  private giveFeedbackAndStayOnCurrentCard(
    feedbackHtml: string | null,
    missingPrerequisiteSkillId: string | null,
    refreshInteraction: boolean,
    refresherExplorationId: string | null
  ) {
    this.numberOfIncorrectSubmissions++;
    this.hintsAndSolutionManagerService.recordWrongAnswer();
    this.conceptCardManagerService.recordWrongAnswer();
    this.playerTranscriptService.addNewResponse(feedbackHtml);
    let helpCardAvailable = false;
    if (feedbackHtml && !this.displayedCard.isInteractionInline()) {
      helpCardAvailable = true;
    }

    if (helpCardAvailable) {
      this.playerPositionService.onHelpCardAvailable.emit({
        helpCardHtml: feedbackHtml,
        hasContinueButton: false,
      });
    }
    if (missingPrerequisiteSkillId) {
      this.displayedCard.markAsCompleted();
      this.conceptCardBackendApiService
        .loadConceptCardsAsync([missingPrerequisiteSkillId])
        .then(conceptCardObject => {
          this.conceptCard = conceptCardObject[0];
          if (helpCardAvailable) {
            this.playerPositionService.onHelpCardAvailable.emit({
              helpCardHtml: feedbackHtml,
              hasContinueButton: true,
            });
          }
        });
    }
    if (refreshInteraction) {
      // Replace the previous interaction with another of the
      // same type.
      this._nextFocusLabel = this.focusManagerService.generateFocusLabel();
      this.playerTranscriptService.updateLatestInteractionHtml(
        this.displayedCard.getInteractionHtml() + this._getRandomSuffix()
      );
    }

    this.redirectToRefresherExplorationConfirmed = false;

    if (refresherExplorationId) {
      // TODO(bhenning): Add tests to verify the event is
      // properly recorded.
      let confirmRedirection = () => {
        this.redirectToRefresherExplorationConfirmed = true;
        this._recordLeaveForRefresherExp(refresherExplorationId);
      };
      this.explorationSummaryBackendApiService
        .loadPublicExplorationSummariesAsync([refresherExplorationId])
        .then(response => {
          if (response.summaries.length > 0) {
            this.refresherExplorationConfirmationModalService.displayRedirectConfirmationModal(
              refresherExplorationId,
              confirmRedirection
            );
          }
        });
    }
    this.focusManagerService.setFocusIfOnDesktop(this._nextFocusLabel);
    this.scrollToBottom();
  }

  private moveToNewCard(
    feedbackHtml: string | null,
    isFinalQuestion: boolean,
    nextCard: StateCard
  ) {
    // There is a new card. If there is no feedback, move on
    // immediately. Otherwise, give the learner a chance to read
    // the feedback, and display a 'Continue' button.
    this.pendingCardWasSeenBefore = false;
    this.displayedCard.markAsCompleted();
    if (isFinalQuestion) {
      if (this.explorationPlayerStateService.isInQuestionPlayerMode()) {
        // We will redirect to the results page here.
        this.questionSessionCompleted = true;
      }
      this.moveToExploration = true;
      if (feedbackHtml) {
        this.playerTranscriptService.addNewResponse(feedbackHtml);
        if (!this.displayedCard.isInteractionInline()) {
          this.playerPositionService.onHelpCardAvailable.emit({
            helpCardHtml: feedbackHtml,
            hasContinueButton: true,
          });
        }
      } else {
        this.showUpcomingCard();
      }
      this.answerIsBeingProcessed = false;
      return;
    }
    this.fatigueDetectionService.reset();
    this.numberAttemptsService.reset();

    let _isNextInteractionInline = this.nextCard.isInteractionInline();
    this.upcomingInlineInteractionHtml = _isNextInteractionInline
      ? this.nextCard.getInteractionHtml()
      : '';
    this.upcomingInteractionInstructions =
      this.nextCard.getInteractionInstructions();

    if (feedbackHtml) {
      if (
        this.playerTranscriptService.hasEncounteredStateBefore(
          nextCard.getStateName()
        )
      ) {
        this.pendingCardWasSeenBefore = true;
      }
      this.playerTranscriptService.addNewResponse(feedbackHtml);
      if (!this.displayedCard.isInteractionInline()) {
        this.playerPositionService.onHelpCardAvailable.emit({
          helpCardHtml: feedbackHtml,
          hasContinueButton: true,
        });
      }
      this.playerPositionService.onNewCardAvailable.emit();
      this._nextFocusLabel =
        ExplorationPlayerConstants.CONTINUE_BUTTON_FOCUS_LABEL;
      this.focusManagerService.setFocusIfOnDesktop(this._nextFocusLabel);
      this.scrollToBottom();
    } else {
      this.playerTranscriptService.addNewResponse(feedbackHtml);
      // If there is no feedback, it immediately moves on
      // to next card. Therefore this.answerIsCorrect needs
      // to be set to false before it proceeds to next card.
      this.answerIsCorrect = false;
      this.showPendingCard();
    }
    this.currentInteractionService.clearPresubmitHooks();
  }

  showPendingCard(): void {
    this.startCardChangeAnimation = true;
    this.explorationPlayerStateService.recordNewCardAdded();

    setTimeout(
      () => {
        this._addNewCard(this.nextCard);

        this.upcomingInlineInteractionHtml = null;
        this.upcomingInteractionInstructions = null;
      },
      0.1 * TIME_FADEOUT_MSEC + 0.1 * TIME_HEIGHT_CHANGE_MSEC
    );

    setTimeout(
      () => {
        this.focusManagerService.setFocusIfOnDesktop(this._nextFocusLabel);
        this.scrollToTop();
      },
      0.1 * TIME_FADEOUT_MSEC + TIME_HEIGHT_CHANGE_MSEC + 0.5 * TIME_FADEIN_MSEC
    );

    setTimeout(
      () => {
        this.startCardChangeAnimation = false;
      },
      0.1 * TIME_FADEOUT_MSEC +
        TIME_HEIGHT_CHANGE_MSEC +
        TIME_FADEIN_MSEC +
        this.TIME_PADDING_MSEC
    );

    this.playerPositionService.onNewCardOpened.emit(this.nextCard);
  }

  showUpcomingCard(): void {
    let currentIndex = this.playerPositionService.getDisplayedCardIndex();
    let conceptCardIsBeingShown =
      this.displayedCard.getStateName() === null &&
      !this.explorationPlayerStateService.isInQuestionMode();
    if (
      conceptCardIsBeingShown &&
      this.playerTranscriptService.isLastCard(currentIndex)
    ) {
      this.returnToExplorationAfterConceptCard();
      return;
    }
    if (this.questionSessionCompleted) {
      this.questionPlayerStateService.onQuestionSessionCompleted.emit(
        this.questionPlayerStateService.getQuestionPlayerStateData()
      );
      return;
    }
    if (this.moveToExploration) {
      this.moveToExploration = false;
      this.explorationPlayerStateService.moveToExploration(
        this._initializeDirectiveComponents.bind(this)
      );
      return;
    }
    if (
      this.displayedCard.isCompleted() &&
      this.nextCard.getStateName() === this.displayedCard.getStateName() &&
      this.conceptCard
    ) {
      this.explorationPlayerStateService.recordNewCardAdded();
      this._addNewCard(
        StateCard.createNewCard(
          null,
          this.conceptCard.getExplanation().html,
          null,
          null,
          null,
          null,
          this.audioTranslationLanguageService
        )
      );
      return;
    }
    if (this.isLearnAgainButton()) {
      const indexOfRevisionCard =
        this.playerTranscriptService.findIndexOfLatestStateWithName(
          this.nextCard.getStateName()
        );
      if (indexOfRevisionCard !== null) {
        this.displayedCard.markAsNotCompleted();
        this.changeCard(indexOfRevisionCard);
        return;
      }
    }
    /* This is for the following situation:
        if A->B->C is the arrangement of cards and C redirected to A,
        then after this, B and C are visited cards and hence
        pendingCardWasSeenBefore would be true during both these
        transitions and as answerIsCorrect is set to false below,
        Continue would briefly change to Learn Again (after it is
        clicked) during these transitions which is not required.
        Also, if the 'if' check is not there, Learn Again button would
        briefly switched to Continue before going to next card. */
    if (this.answerIsCorrect) {
      this.pendingCardWasSeenBefore = false;
    }
    this.answerIsCorrect = false;
    this.showPendingCard();
  }

  scrollToBottom(): void {
    setTimeout(() => {
      let tutorCard = $('.conversation-skin-main-tutor-card');

      if (tutorCard && tutorCard.length === 0) {
        return;
      }
      let tutorCardBottom = tutorCard.offset().top + tutorCard.outerHeight();
      if ($(window).scrollTop() + $(window).height() < tutorCardBottom) {
        $('html, body').animate(
          {
            scrollTop: tutorCardBottom - $(window).height() + 12,
          },
          {
            duration: this.TIME_SCROLL_MSEC,
            easing: 'easeOutQuad',
          }
        );
      }
    }, 100);
  }

  scrollToTop(): void {
    setTimeout(() => {
      $('html, body').animate(
        {
          scrollTop: 0,
        },
        800,
        'easeOutQuart'
      );
      return false;
    });
  }

  onNavigateFromIframe(): void {
    this.siteAnalyticsService.registerVisitOppiaFromIframeEvent(
      this.explorationId
    );
  }

  submitAnswerFromProgressNav(): void {
    this.displayedCard.toggleSubmitClicked(true);
    this.currentInteractionService.submitAnswer();
  }

  getRecommendedExpTitleTranslationKey(explorationId: string): string {
    return this.i18nLanguageCodeService.getExplorationTranslationKey(
      explorationId,
      TranslationKeyType.TITLE
    );
  }

  isHackyExpTitleTranslationDisplayed(explorationId: string): boolean {
    let recommendedExpTitleTranslationKey =
      this.getRecommendedExpTitleTranslationKey(explorationId);
    return (
      this.i18nLanguageCodeService.isHackyTranslationAvailable(
        recommendedExpTitleTranslationKey
      ) && !this.i18nLanguageCodeService.isCurrentLanguageEnglish()
    );
  }

  isDisplayedCardCompletedInPrevSession(): boolean {
    return (
      this.displayedCard.getInteraction() &&
      this.prevSessionStatesProgress.indexOf(
        this.displayedCard.getStateName()
      ) !== -1
    );
  }

  isProgressClearanceMessageShown(): boolean {
    return this.showProgressClearanceMessage;
  }

  // Returns whether the screen is wide enough to fit two
  // cards (e.g., the tutor and supplemental cards) side-by-side.
  canWindowShowTwoCards(): boolean {
    return (
      this.windowDimensionsService.getWidth() >
      ExplorationPlayerConstants.TWO_CARD_THRESHOLD_PX
    );
  }

  animateToTwoCards(doneCallback: () => void): void {
    this.isAnimatingToTwoCards = true;
    setTimeout(
      () => {
        this.isAnimatingToTwoCards = false;
        if (doneCallback) {
          doneCallback();
        }
      },
      TIME_NUM_CARDS_CHANGE_MSEC + TIME_FADEIN_MSEC + this.TIME_PADDING_MSEC
    );
  }

  animateToOneCard(doneCallback: () => void): void {
    this.isAnimatingToOneCard = true;
    setTimeout(() => {
      this.isAnimatingToOneCard = false;
      if (doneCallback) {
        doneCallback();
      }
    }, TIME_NUM_CARDS_CHANGE_MSEC);
  }
}

angular.module('oppia').directive(
  'oppiaConversationSkin',
  downgradeComponent({
    component: ConversationSkinComponent,
  }) as angular.IDirectiveFactory
);