import { Component, OnDestroy } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { GenerateQuestionsService } from './generate-questions.service'; import { Router } from '@angular/router'; import confetti from 'canvas-confetti'; import { HeaderComponent } from '../shared/header/header.component'; import { ButtonComponent } from '../shared/button/button.component'; interface QuestionWithAnswer { question: string; correctAnswer: string; } interface Question { parts: string[]; index: number; } const COUNTDOWN_SECONDS = 10; const HINT_AUTO_HIDE_MS = 5000; const NEXT_LEVEL_DELAY_MS = 2000; const COMPLETION_DELAY_MS = 3000; const CONFETTI_INTERVAL_MS = 250; const RING_RADIUS = 90; const INCORRECT_INDICATORS = [ 'incorrect', 'not correct', 'wrong', 'not right', 'is not', 'are not', 'isn\'t correct', 'aren\'t correct' ]; const CORRECT_INDICATORS = [ 'correct', 'right', 'yes', 'accurate', 'well done', 'good job', 'perfect' ]; @Component({ selector: 'app-generate-questions', standalone: true, imports: [FormsModule, CommonModule, HeaderComponent, ButtonComponent], templateUrl: './generate-questions.component.html', styleUrls: ['./generate-questions.component.css'] }) export class GenerateQuestionsComponent implements OnDestroy { topic = ''; readonly hardcodedTopics = ['Noun', 'Verb', 'Past Tense', 'Adjective', 'Present Continuous']; questions: Question[] = []; questionsWithAnswers: QuestionWithAnswer[] = []; userAnswers: string[] = []; answerStatuses: ('correct' | 'incorrect' | 'pending')[] = []; feedback: string[] = []; hints: string[] = []; readonlyAnswers: boolean[] = []; attemptCounts: Record = {}; currentDifficulty = 'basic'; readonly difficultyLevels = ['basic', 'intermediate', 'expert']; showSuggestions = false; isQuestionsGenerated = false; isValidationInProgress = false; isFirstAttemptDone = false; isAllLevelsCompleted = false; isHintMenuVisible = false; isLoading = false; isChecked = false; error = ''; get isGenerateDisabled() { return !this.topic.trim() || this.isQuestionsGenerated; } get isResetDisabled() { return !this.topic && !this.isQuestionsGenerated; } get isTopicLocked() { return this.isQuestionsGenerated; } get isDropdownDisabled() { return this.isQuestionsGenerated; } get showLevelTooltip() { return this.isQuestionsGenerated; } get activateLevelDot() { return this.isQuestionsGenerated; } private hasShownHintIcon = false; showHintIcon = false; hasNewHints = false; showGlobalCountdown = false; globalCountdown = 0; overlayCaption = ''; ringCircumference = 2 * Math.PI * RING_RADIUS; ringDashoffset = this.ringCircumference; private globalTimer: any; private overlayTicker: any; private overlayEndTs = 0; private globalCountdownActive = false; private confettiInterval: any; constructor( private service: GenerateQuestionsService, private router: Router ) {} ngOnDestroy(): void { this.clearTimers(); this.stopConfettiAnimation(); } private clearTimers(): void { if (this.globalTimer) clearInterval(this.globalTimer); if (this.overlayTicker) clearInterval(this.overlayTicker); this.globalTimer = null; this.overlayTicker = null; this.showGlobalCountdown = false; this.globalCountdownActive = false; } onTopicChange(): void { this.showSuggestions = !this.topic.trim(); } getProgressWidth(): string { const index = this.difficultyLevels.indexOf(this.currentDifficulty); if (index === -1 || this.difficultyLevels.length <= 1) return '0%'; return `${(index / (this.difficultyLevels.length - 1)) * 100}%`; } generateQuestions(): void { if (this.isAllLevelsCompleted) { this.error = 'Please reset to start over.'; return; } this.resetQuestionState(); this.service.generateQuestions(this.topic, this.currentDifficulty) .subscribe({ next: (response) => this.handleQuestionsResponse(response), error: (error) => this.handleQuestionsError(error) }); } private handleQuestionsResponse(response: any): void { const rawQuestions = (response?.text ?? response?.generations?.[0]?.text ?? '').trim(); if (!rawQuestions) { this.error = 'No questions generated. Please try again.'; return; } this.isQuestionsGenerated = true; this.parseAndInitializeQuestions(rawQuestions); } private handleQuestionsError(error: any): void { this.error = error.status === 400 && error.error.message ? error.error.message : 'Failed to fetch questions. Please try again later.'; } private parseAndInitializeQuestions(rawQuestions: string): void { this.questionsWithAnswers = this.parseQuestions(rawQuestions); this.questions = this.splitQuestionsIntoParts(this.questionsWithAnswers); this.initializeAnswerArrays(this.questions.length); } private parseQuestions(text: string): QuestionWithAnswer[] { const regex = /(\d+\.\s*.+?_______)\s*(.*?)\s*\(([^)]+)\)\s*$/gm; const questions: QuestionWithAnswer[] = []; let match; while ((match = regex.exec(text)) !== null) { const questionText = match[1]?.trim(); const afterBlank = match[2]?.replace(/^[_-]+/, '').replace(/^\s+/, ' ').replace(/\s{2,}/g, ' '); const correctAnswer = match[3]?.trim(); if (questionText && correctAnswer) { questions.push({ question: `${questionText}${afterBlank}`, correctAnswer }); } } return questions; } private splitQuestionsIntoParts(questions: QuestionWithAnswer[]): Question[] { return questions.map((q, index) => { const normalized = q.question.replace(/\s*_{3,}\s*/g, '_______'); const parts = normalized.split('_______').map(p => p.replace(/[_]+/g, '').trim()); return { parts, index }; }); } private initializeAnswerArrays(length: number): void { this.userAnswers = Array(length).fill(''); this.answerStatuses = Array(length).fill('pending'); this.feedback = Array(length).fill(''); this.readonlyAnswers = Array(length).fill(false); this.hints = []; this.attemptCounts = {}; for (let i = 0; i < length; i++) { this.attemptCounts[i] = 0; } } private resetQuestionState(): void { this.hints = []; this.showSuggestions = false; this.readonlyAnswers = []; } resetTopic(): void { this.topic = ''; this.questions = []; this.questionsWithAnswers = []; this.userAnswers = []; this.answerStatuses = []; this.readonlyAnswers = []; this.feedback = []; this.hints = []; this.attemptCounts = {}; this.isQuestionsGenerated = false; this.isValidationInProgress = false; this.isFirstAttemptDone = false; this.isAllLevelsCompleted = false; this.hasNewHints = false; this.showSuggestions = false; this.isChecked = false; this.currentDifficulty = 'basic'; this.error = ''; this.clearTimers(); this.stopConfetti(); } areAllAnswersFilled(): boolean { return this.userAnswers.every(answer => answer?.trim()); } closeErrorPopup(): void { this.error = ''; } private parseAIValidation(response: string): boolean { if (!response) return false; const lower = response.toLowerCase(); if (INCORRECT_INDICATORS.some(ind => lower.includes(ind))) return false; if (CORRECT_INDICATORS.some(ind => lower.includes(ind))) return true; return false; } checkAllAnswers(): void { this.isValidationInProgress = true; this.isLoading = true; this.isFirstAttemptDone = true; const payload = this.questions.map((q, i) => ({ topic: this.topic, question: q.parts.join('_______'), user_answer: this.userAnswers[i] })); this.service.validateAllAnswers(payload).subscribe({ next: (response) => this.handleValidationResponse(response), error: (error) => this.handleValidationError(error) }); } private handleValidationResponse(response: any): void { if (!response.results?.length) { this.error = 'Failed to validate answers. Please try again.'; this.resetValidationState(); return; } let hadFirstWrong = false; let hadSecondWrong = false; response.results.forEach((result: any, index: number) => { this.attemptCounts[index]++; const isCorrect = this.parseAIValidation(result.validation_response); this.feedback[index] = result.validation_response || 'No feedback provided.'; if (isCorrect) { this.markAsCorrect(index); } else { const attemptResult = this.handleIncorrectAttempt(index, result.hint); hadFirstWrong = hadFirstWrong || attemptResult.first; hadSecondWrong = hadSecondWrong || attemptResult.second; } }); this.handleHintsDisplay(hadFirstWrong || hadSecondWrong); if (!hadSecondWrong) { this.checkLevelCompletion(); } this.resetValidationState(); } private markAsCorrect(index: number): void { this.readonlyAnswers[index] = true; this.hints[index] = ''; this.answerStatuses[index] = 'correct'; this.userAnswers[index] = this.userAnswers[index].trim(); } private handleIncorrectAttempt(index: number, hint: string): { first: boolean; second: boolean } { this.answerStatuses[index] = 'incorrect'; this.hints[index] = hint?.trim() || ''; if (this.attemptCounts[index] === 1) { this.scheduleAnswerClear(index); return { first: true, second: false }; } else if (this.attemptCounts[index] >= 2) { this.scheduleAnswerReveal(index); return { first: false, second: true }; } return { first: false, second: false }; } private scheduleAnswerClear(index: number): void { if (!this.globalCountdownActive) this.startOverlay('Try again in'); setTimeout(() => { this.userAnswers[index] = ''; this.answerStatuses[index] = 'pending'; this.clearTimers(); }, COUNTDOWN_SECONDS * 1000); } private scheduleAnswerReveal(index: number): void { if (!this.globalCountdownActive) this.startOverlay('Showing correct answer in'); setTimeout(() => { this.userAnswers[index] = this.questionsWithAnswers[index].correctAnswer; this.answerStatuses[index] = 'correct'; this.readonlyAnswers[index] = true; this.clearTimers(); if (this.areAllCorrectAnswersDisplayed()) { if (this.isLastLevel()) { this.scheduleCompletion(); } else { this.scheduleNextLevel(); } } }, COUNTDOWN_SECONDS * 1000); } private scheduleCompletion(): void { this.startOverlay('Finishing in'); setTimeout(() => { this.clearTimers(); this.isAllLevelsCompleted = true; this.triggerConfetti(); }, COUNTDOWN_SECONDS * 1000); } private handleHintsDisplay(hasWrongAnswers: boolean): void { this.hasNewHints = this.hints.some(h => h?.trim()); if (this.hasNewHints) { setTimeout(() => { this.hasNewHints = false; }, HINT_AUTO_HIDE_MS); } if (!this.hasShownHintIcon && (this.hasNewHints || hasWrongAnswers)) { this.showHintIcon = true; this.hasShownHintIcon = true; } } private checkLevelCompletion(): void { if (this.areAllCorrectAnswersDisplayed()) { if (this.isLastLevel()) { this.scheduleCompletion(); } else { this.scheduleNextLevel(); } } } private scheduleNextLevel(): void { this.clearTimers(); this.startOverlay('Moving to next level in'); setTimeout(() => { this.clearTimers(); setTimeout(() => this.transitionDifficulty(), NEXT_LEVEL_DELAY_MS); }, COUNTDOWN_SECONDS * 1000); } private handleValidationError(error: any): void { this.error = 'Error validating answers. Please try again later.'; this.resetValidationState(); } private resetValidationState(): void { this.isValidationInProgress = false; this.isLoading = false; } isLastLevel(): boolean { return this.currentDifficulty === this.difficultyLevels[this.difficultyLevels.length - 1]; } areAllCorrectAnswersDisplayed(): boolean { return this.readonlyAnswers.every(readonly => readonly); } transitionDifficulty(): void { const currentIndex = this.difficultyLevels.indexOf(this.currentDifficulty); if (currentIndex < this.difficultyLevels.length - 1) { this.currentDifficulty = this.difficultyLevels[currentIndex + 1]; this.generateQuestions(); } else { setTimeout(() => { this.questions = []; this.isAllLevelsCompleted = true; this.triggerConfetti(); }, COMPLETION_DELAY_MS); } } onAnswerChange(index: number): void { this.userAnswers[index] = this.userAnswers[index]?.trim() || ''; this.userAnswers = [...this.userAnswers]; } goToHome(): void { this.router.navigate(['/home']); } triggerConfetti(): void { if (this.confettiInterval) clearInterval(this.confettiInterval); this.confettiInterval = setInterval(() => { confetti({ startVelocity: 30, spread: 360, ticks: 60, origin: { x: Math.random(), y: Math.random() - 0.2 } }); }, CONFETTI_INTERVAL_MS); } stopConfetti(): void { this.isAllLevelsCompleted = false; this.stopConfettiAnimation(); } private stopConfettiAnimation(): void { if (this.confettiInterval) { clearInterval(this.confettiInterval); this.confettiInterval = null; } document.querySelector('canvas.confetti-canvas')?.remove(); document.querySelectorAll('.confetti, .ts-confetti').forEach(el => el.remove()); } stopConfettiAndReset(): void { this.stopConfetti(); this.resetTopic(); } selectTopic(suggestion: string): void { this.topic = suggestion; this.showSuggestions = false; } hideSuggestions(): void { setTimeout(() => { this.showSuggestions = false; }, 150); } closeHints(): void { this.isHintMenuVisible = false; this.hasNewHints = false; } toggleHintMenu(): void { this.isHintMenuVisible = !this.isHintMenuVisible; if (this.isHintMenuVisible) this.showHintIcon = false; } onInput(event: any, index: number): void { const filtered = event.target.value.replace(/[^a-zA-Z]/g, ''); this.userAnswers[index] = filtered; event.target.value = filtered; } private startOverlay(caption: string, seconds = COUNTDOWN_SECONDS): void { this.clearTimers(); this.globalCountdownActive = true; this.showGlobalCountdown = true; this.overlayCaption = caption; this.globalCountdown = seconds; this.overlayEndTs = Date.now() + seconds * 1000; this.ringDashoffset = this.ringCircumference; this.overlayTicker = setInterval(() => { const now = Date.now(); const remainingMs = Math.max(0, this.overlayEndTs - now); const elapsedMs = seconds * 1000 - remainingMs; this.globalCountdown = Math.ceil(remainingMs / 1000); this.ringDashoffset = this.ringCircumference * (1 - Math.min(1, elapsedMs / (seconds * 1000))); if (remainingMs <= 0) this.clearTimers(); }, 50); this.globalTimer = setTimeout(() => this.clearTimers(), seconds * 1000); } }