File

src/app/models/scenario.ts

Description

Classe générique pour messages commun à toutes les activités.

Properties

parentLevel
parentLevel: number
phrase
phrase: string
priority
priority: number
tags
tags: object
import { PlayTTSService } from "src/app/services/play-tts.service";
import { ChangeDetectorRef } from "@angular/core";
import { CabriDataService } from "../services/cabri-data.service";
import { GlobalService } from "../services/global.service";
import { ScenarioPhrase } from "./scenario-phrase";
import { Tutorial } from "./tutorial";
import { environment } from "src/environments/environment";
import { AccountService } from "../services/account.service";
import { AppUtils } from "../app-utils";
import { ActivityAnswer, AnswerAwardTrans, AnswerNeedsHelp, AnswerSpeed, AnswerStatus } from "./activity-answer";
import { ReplaySubject } from "rxjs";

/**
 * Classe générique pour messages commun à toutes les activités.
 */

export class Feedback {
	phrase: string;
	tags: object;
	priority?: number;
	parentLevel: number;
}
export class Scenario {
	public page: any;
	protected cabriService: CabriDataService;
	protected globalService: GlobalService;
	protected cd: ChangeDetectorRef;

	// callback tuto
	public tutoCallbackSave: Tutorial;
	public codeReceived: boolean;
	public mathiaMessage: string;
	public mscPhraseArrayIndex = -1;
	public lastPhrase: string;
	public skipSequence = false;
	public lockCallback = false;
	appName: string;
	environment: any;
	accountService: AccountService;
	resolveTimeoutCancelable: (value: unknown) => void;

	public answer: ActivityAnswer;
	feedbacks: Array<Feedback>;
	lastFeedbackPhrasesPlayed: Array<string>;

	currentRunMathiaSpeechPromise: Array<{ promise: Promise<void>; scenario: any }> = [];

	constructor(
		accountService: AccountService,
		globalService: GlobalService,
		page: any,
		cd: ChangeDetectorRef,
		public ttsService: PlayTTSService
	) {
		this.environment = environment;

		this.accountService = accountService;
		this.globalService = globalService;
		this.page = page;
		this.cd = cd;
		this.lastFeedbackPhrasesPlayed = new Array();
	}

	runBabylonPageScript(): Promise<void> {
		return new Promise(resolve => {
			resolve();
		});
	}

	/**
	 * Custom async setTimeOut for waiting end of exe - skipable by skipSequence()
	 * @ignore
	 */
	async timeOut(ms, callback: any = null) {
		return new Promise(resolve => {
			if (!this.skipSequence) {
				setTimeout(() => {
					if (callback) {
						callback();
					}
					resolve(true);
				}, ms);
			} else {
				setTimeout(() => {
					if (callback) {
						callback();
					}
					resolve(true);
				}, 100);
			}
		});
	}

	/**
	 * Custom async setTimeOut for waiting end of exe - cancelable from outside triggering skipScenarioTimeout()
	 * @ignore
	 */
	async timeOutCancelable(ms, callback: any = null) {
		return new Promise(resolve => {
			this.resolveTimeoutCancelable = resolve;
			if (!this.skipSequence) {
				setTimeout(() => {
					if (callback) {
						callback();
					}
					this.resolveTimeoutCancelable = null;
					resolve(true);
				}, ms);
			} else {
				setTimeout(() => {
					if (callback) {
						callback();
					}
					this.resolveTimeoutCancelable = null;
					resolve(true);
				}, 100);
			}
		});
	}

	/**
	 * cancel timeOutCancelable()
	 * @ignore
	 */
	skipScenarioTimeout() {
		if (this.resolveTimeoutCancelable) {
			this.resolveTimeoutCancelable(true);
			this.resolveTimeoutCancelable = null;
		}
	}

	/**
	 * Custom async setTimeOut for waiting end of exe - not skipable
	 * @ignore
	 */
	async timeOutNoSkip(ms, callback: any = null) {
		return new Promise(resolve => {
			setTimeout(() => {
				if (callback) {
					callback();
				}
				resolve(true);
			}, ms);
		});
	}

	detectChanges() {
		if (this.cd) {
			this.cd.detectChanges();
		}
	}

	/**
	 * @ignore
	 */
	nextPlayerInit(): Promise<void> {
		return new Promise(async (resolve, reject) => {
			resolve();
		});
	}

	/**
	 * Main method used to run TTS scenarios with callbacks
	 * @param content scenario sequence array
	 * @param globalBubble not in use here
	 * @param fromToolbar obsolete?
	 */
	runMathiaSpeech(content: any[], globalBubble?: boolean, fromToolbar = false): Promise<void> {
		const p = new Promise<void>((resolve, reject) => {
			console.log("runMathiaSpeech entered");
			this.globalService.mathiaSpeechRunning = true;
			let lastElement;

			const contentClone = content.slice(0);
			contentClone.reverse().forEach(element => {
				const current = new Tutorial();
				current.phrase = element.phrase;
				if (element.phraseTTS) {
					current.phraseTTS = element.phraseTTS;
				}
				if (element.onlySpeech) {
					current.onlySpeech = element.onlySpeech;
				}
				if (element.randomSpeechMode) {
					current.randomSpeechMode = element.randomSpeechMode;
				}
				if (element.textBubble) {
					current.textBubble = element.textBubble;
				}
				if (element.buttons) {
					current.buttons = element.buttons;
				}
				if (element.disableSkip) {
					current.disableSkip = element.disableSkip;
				}
				if (element.buttonText) {
					current.buttonText = element.buttonText;
				}
				if (element.shootingStar) {
					current.shootingStar = element.shootingStar;
				}
				if (element.normalStar) {
					current.normalStar = element.normalStar;
				}
				if (element.moon) {
					current.moon = element.moon;
				}
				if (element.keepBubble) {
					current.keepBubble = element.keepBubble;
				}
				//array read in reverse so the first tutorial is the end and need to resolve
				if (!lastElement) {
					current.callback = async () => {
						if (element.callback) {
							if (typeof element.callback === "function" && !this.lockCallback) {
								await element.callback();
							}
						}
						this.globalService.mathiaSpeechRunning = false;
						this.skipSequence = false;
						resolve();
					};
				} else {
					current.callback = async () => {
						if (element.callback) {
							if (typeof element.callback === "function" && !this.lockCallback) {
								await element.callback();
							}
						} else {
							resolve();
						}
					};
				}
				current.next = lastElement;
				lastElement = current;
			});
			if (this.lockCallback) {
				console.error("rejected by lockCallback");
				reject();
			} else {
				this.runMathiaSpeechCore(lastElement, fromToolbar);
			}
		});

		this.currentRunMathiaSpeechPromise.push({ promise: p, scenario: content });
		return p;
	}

	/**
	 * @ignore
	 */
	runMathiaSpeechCore(tutorial: Tutorial, fromToolbar = false, eventProtected = true): void {
		if (tutorial) {
			if (Array.isArray(tutorial.phrase) && tutorial.phrase.length > 1) {
				if (tutorial.randomSpeechMode) {
					// select random index from tutorial.phrase array
					const randomIndex = Math.floor(Math.random() * tutorial.phrase.length);
					// console.log("random index = " + randomIndex);oe
					tutorial.phrase = tutorial.phrase[randomIndex];
					if (tutorial.phraseTTS) {
						tutorial.phraseTTS = tutorial.phraseTTS[randomIndex];
					}
				} else {
					// increment index of tutorial.phrase array
					this.mscPhraseArrayIndex++;
					if (this.mscPhraseArrayIndex >= tutorial.phrase.length) {
						this.mscPhraseArrayIndex = 0;
					}
					tutorial.phrase = tutorial.phrase[this.mscPhraseArrayIndex];
					if (tutorial.phraseTTS) {
						tutorial.phraseTTS = tutorial.phraseTTS[this.mscPhraseArrayIndex];
					}
				}
			} else if (Array.isArray(tutorial.phrase) && tutorial.phrase.length === 1) {
				tutorial.phrase = tutorial.phrase[0];
				if (tutorial.phraseTTS) {
					tutorial.phraseTTS = tutorial.phraseTTS[0];
				}
			}
			this.page?.eventMessage?.next(tutorial.phrase);
			this.lastPhrase = tutorial.phrase;
			if (!tutorial.onlySpeech && this.page?.displayTTSBubble) {
				this.page.displayTTSBubble = true;
			}
			if (!this.skipSequence && this.cd) {
				this.detectChanges();
			}
			if (!this.skipSequence && !this.lockCallback) {
				this.play(
					tutorial,
					async () => {
						tutorial.callback().then(() => {
							this.page.mathiaSpeechButtonText = null;
							if (!this.page.menuOpen && !this.globalService.appIdle) {
								this.detectChanges();
								return this.runMathiaSpeechCore(tutorial.next, fromToolbar);
							} else {
								this.detectChanges();
								this.tutoCallbackSave = tutorial.next;
							}
						});
					},
					fromToolbar,
					eventProtected
				);
			} else if (this.skipSequence === true && !this.lockCallback) {
				tutorial.callback().then(async () => {
					this.page.mathiaSpeechButtonText = null;
					if (!this.page.menuOpen && !this.globalService.appIdle) {
						return this.runMathiaSpeechCore(tutorial.next, fromToolbar);
					} else {
						this.detectChanges();
						this.tutoCallbackSave = tutorial.next;
					}
				});
			}
		}
	}

	play(tutorial: Tutorial, callback, fromToolbar, eventProtected) {
		if (eventProtected) {
			this.ttsService.playTTSEventProtected(tutorial, callback, this.ttsService.dicteeMode, fromToolbar);
		} else {
			this.ttsService.playTTS(tutorial.phraseTTS).then(() => {
				if (callback) {
					callback();
				}
			});
		}
	}
	/**
	 * method to transform a given text into a scenario sequence by splitting it depending of punctuation
	 * @param text text to transform
	 * @param fromToolbar obsolete?
	 */
	async readText(text, fromToolbar = false, globalBubble = false, keepBubbleOn = false) {
		const sequences = this.splitTextPunctationToScenarioPhraseArray(text, keepBubbleOn);
		await this.runMathiaSpeech(sequences, globalBubble, fromToolbar);
	}

	/**
	 * skip the scenario sequence on skip button click or manual call from anywhere
	 * @param data
	 */
	async skipMathiaSpeechSequence(data: any = true) {
		if (this.ttsService.protectedTTSisPlaying) {
			this.skipSequence = data;
			await this.ttsService.killSpeech();

			if (this.ttsService.currentTTSPCallback) {
				await this.ttsService.endOfTTSPlayprotected();
				// console.error("ok endOfTTSPlayprotected")
			}

			// console.error("before all promises", this.currentRunMathiaSpeechPromise);
			const promises = this.currentRunMathiaSpeechPromise.map(m => m.promise);
			this.currentRunMathiaSpeechPromise = [];
			//await Promise.all(promises);
			let security = true;
			if (promises.length > 0) {
				await Promise.race([
					Promise.all(promises),
					AppUtils.timeOut(5000, () => {
						if (security) {
							console.error("timeout promises skipMathiaSpeechSequence");
						}
					})
				]);
			}
			security = false;

			if (this.ttsService.currentTTSPCallback) {
				await this.ttsService.endOfTTSPlayprotected();
				// console.error("ok endOfTTSPlayprotected")
			}

			// console.error("ok all promises")
		}
	}

	/**
	 * splits given text depending on punctuation and returns a SpeechSequence array
	 */
	splitTextPunctationToScenarioPhraseArray(text: string, keepBubbleOn = false) {
		const speechSequence = [];
		text = text.replace("...", "…");
		if (text.match(/(\.|\?|\!)/g)) {
			// Split feedback with punctuation:
			const feedbackSequence = text.split(/(\.|\?|\!)/g);
			// move punctuation index content to the end of the previous index:
			feedbackSequence.forEach((phrase, index) => {
				if (phrase.match(/(\.|\?|\!)/g)) {
					feedbackSequence[index - 1] = feedbackSequence[index - 1] + phrase;
					feedbackSequence.splice(index, 1);
				}
			});
			feedbackSequence.forEach((phrase, index) => {
				if (phrase.length === 0 || phrase.trim().length === 0) {
					// remove empty or blank indexes:
					feedbackSequence.splice(index, 1);
				} else if (/^\s/.test(phrase)) {
					// remove empty space at the beginning of phrases:
					phrase = phrase.substring(1);
					feedbackSequence.splice(index, 1, phrase);
				}
			});
			for (const phrase of feedbackSequence) {
				if (keepBubbleOn) {
					speechSequence.push(new ScenarioPhrase(phrase).keepBubbleOn());
				} else {
					speechSequence.push(new ScenarioPhrase(phrase));
				}
			}
		} else {
			if (keepBubbleOn) {
				speechSequence.push(new ScenarioPhrase(text).keepBubbleOn());
			} else {
				speechSequence.push(new ScenarioPhrase(text));
			}
		}
		return speechSequence;
	}
	// runMathiaSpeechCore(tutorial: Tutorial): void {
	// 	throw new Error("not implemented in mother class");
	// }

	/**
	 * read a text through runMathiaSpeech() to be handled as it (pausable by events, display through tts bubble)
	 * @param consigne text to read / display
	 */
	async readCustomText(consigne: string, globalBubble = false) {
		const speechSequence: Array<any> = [new ScenarioPhrase([consigne])];
		await this.runMathiaSpeech(speechSequence, globalBubble);
	}

	/**
	 * convert feedback from fiche raw text to a speechSequence & run it in runMathiaSpeech()
	 * splits punctuation / removeLineBreaksFromString
	 */
	async readHTML(html: string, globalBubble = false) {
		const speechSequence = [];
		const parser = new DOMParser();
		html = AppUtils.removeLineBreaksFromString(parser.parseFromString(html, "text/html").body.textContent);
		if (html.match(/(\.|\?|\!)/g)) {
			// Split feedback with punctuation:
			const sequence = html.split(/(\.|\?|\!)/g);

			// move punctuation index content to the end of the previous index:
			sequence.forEach((phrase, index) => {
				if (phrase.match(/(\.|\?|\!)/g)) {
					sequence[index - 1] = sequence[index - 1] + phrase;
					sequence.splice(index, 1);
				}
			});
			sequence.forEach((phrase, index) => {
				if (phrase.length === 0 || phrase.trim().length === 0) {
					// remove empty or blank indexes:
					sequence.splice(index, 1);
				} else if (/^\s/.test(phrase)) {
					// remove empty space at the beginning of phrases:
					phrase = phrase.substring(1);
					sequence.splice(index, 1, phrase);
				}
			});
			for (const [index, iterator] of sequence.entries()) {
				speechSequence.push(new ScenarioPhrase(iterator).keepBubbleOn());
			}
		} else {
			speechSequence.push(new ScenarioPhrase(html).keepBubbleOn());
		}
		await this.runMathiaSpeech(speechSequence, globalBubble);
	}

	// Dynamic Feedbacks

	/**
	 * get a feedback phrase depending of parameters and answer's context
	 */
	getContextualFeedbackPhrase(
		status: AnswerStatus,
		responseTimeInSeconds: number,
		award?: string,
		slow = 20,
		fast = 7,
		minAnswersForAverage = 5,
		minAnswersForExtreme = 5,
		needsHelp = AnswerNeedsHelp.NO
	) {
		this.answer = new ActivityAnswer(
			status,
			needsHelp,
			responseTimeInSeconds,
			slow,
			fast,
			this.page.currentUser,
			minAnswersForAverage,
			minAnswersForExtreme,
			this.cabriService.currentExercice
		);
		// AppUtils.debug("this.answer = ", this.answer);
		const contextTags = this.answer.getContext();
		const feedbacks = this.getFilteredFeedbacksByContext(contextTags);
		const sortedByPriorityFeedbacks = this.filterFeedbacksByPriority(feedbacks);
		// AppUtils.debug("sortedByPriorityFeedbacks = ", sortedByPriorityFeedbacks);
		const selectedFeedback = this.selectFeedbackAvoidingRepetition(sortedByPriorityFeedbacks);
		let phrase = selectedFeedback.phrase;
		if (award) {
			const awardName = this.answer.setAnswerAward(award);
			const trad = AnswerAwardTrans[awardName];
			phrase = phrase.replace(/#award/, trad);
		}
		phrase = phrase.replace(/#playerName/, this.page.currentUser.name);
		return phrase;
	}

	// TODO => special method for not answer related phrases using same mechanism ?
	getSpecialPhrase() {
		// TODO
	}

	/**
	 * filter feedbacks by given context tags
	 * exclusive is tag set to false
	 */
	getFilteredFeedbacksByContext(tags: object): Feedback[] {
		const filteredFeedbacks = this.feedbacks.filter(feedback => {
			return Object.entries(tags).every(tag => {
				const key = tag[0];
				const value = tag[1];
				// console.error("getFeedbacks() loop");
				if (typeof feedback.tags[key] !== "undefined") {
					if (feedback.tags[key] === value) {
						return true;
					} else {
						return false;
					}
				} else {
					return true;
				}
			});
		});
		return filteredFeedbacks;
	}

	/**
	 * sorts feedbacks by priority + parent level:
	 * shuffle disabled to test phrase indexes sequencing
	 */
	filterFeedbacksByPriority(feedbacks: Feedback[]): Feedback[] {
		// AppUtils.shuffleArray(feedbacks);
		feedbacks.sort((a: Feedback, b: Feedback) => (a.parentLevel > b.parentLevel ? -1 : 1));
		// console.error(feedbacks);
		feedbacks.sort((a, b) => (a.priority > b.priority ? -1 : 1));
		// console.error("feedbacks sorted = ", feedbacks);
		return feedbacks;
	}

	/**
	 * checks if feedback has been played before (history length = 5) else pick another
	 */
	selectFeedbackAvoidingRepetition(feedbacks: Feedback[]): Feedback {
		return feedbacks.find(feedback => {
			const tagsKey = JSON.stringify(feedback.tags);
			// console.error("tagsKey = ", tagsKey);
			const readFeedbacks = this.lastFeedbackPhrasesPlayed[tagsKey];
			if (readFeedbacks && readFeedbacks.indexOf(feedback.phrase) > -1) {
				// already read
				return false;
			} else {
				// initialize array
				if (!this.lastFeedbackPhrasesPlayed[tagsKey]) {
					this.lastFeedbackPhrasesPlayed[tagsKey] = [];
				}
				// add new feedback to read phrase
				this.lastFeedbackPhrasesPlayed[tagsKey].unshift(feedback.phrase);
				// max 5 feedbacks in history
				let maxPhrases = 5;
				if (tagsKey === `{"status":"${AnswerStatus.VALID1ST}","speed":"${AnswerSpeed.FAST}"}`) {
					maxPhrases = 12;
				}
				if (this.lastFeedbackPhrasesPlayed[tagsKey].length > Math.min(maxPhrases, feedbacks.length - 1)) {
					// remove oldest read feedback
					this.lastFeedbackPhrasesPlayed[tagsKey].pop();
				}
				// AppUtils.debug("last Feedbacks = ", this.lastFeedbackPhrasesPlayed);
				return true;
			}
		});
	}

	/**
	 * Adds feedback array with params to this.feedbacks global Array
	 */
	addFeedbacks(tags: object, priority: number, phrases: Array<string>, parentLevel = 1) {
		phrases.forEach(phrase => {
			// console.error("addFeedbacks()");
			this.feedbacks.push({ phrase, tags, priority, parentLevel });
		});
	}

	/**
	 * overloaded upstairs
	 */
	populateScenario() {}
}

results matching ""

    No results matching ""