py-learn / src /app /generate-questions /generate-questions.component.ts
Oviya
fix
9db5bb0
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<number, number> = {};
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);
}
}