py-learn / src /app /reading /reading.component.ts
Anupriya
reading component back arrow
a998588
import { Component, ElementRef, ViewChild } from '@angular/core';
import { ReadingService } from './reading.service';
import { Router } from '@angular/router';
import confetti from 'canvas-confetti';
import { ButtonComponent } from '../shared/button/button.component';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HeaderComponent } from '../shared/header/header.component';
@Component({
selector: 'app-reading',
templateUrl: './reading.component.html',
styleUrl: './reading.component.css',
standalone: true,
imports: [ButtonComponent, CommonModule, FormsModule, HeaderComponent]
})
export class ReadingComponent {
loadingQuestions = false;
isGeneratingContent = false;
isGenerateDisabled = false;
isGenerateQuestionDisabled = false;
showPopup = false;
showSuggestions = false;
hasStarted = false;
currentQuestionIndex = 0;
questions: { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] = [];
selectedAnswers: { [key: string]: string } = {};
topic: string = '';
difficulty: 'easy' | 'medium' | 'hard' = 'easy';
content: string = '';
errorMessage: string = '';
normalizedTopic: string = '';
filteredSuggestions: string[] = [];
activeIndex = -1;
fontPx = parseInt(localStorage.getItem('passageFontPx') || '18', 10);
isReading = false;
ttsPaused = false;
private ttsUtterance?: SpeechSynthesisUtterance;
topicsByDifficulty: Record<'easy' | 'medium' | 'hard', string[]> = {
easy: [
'My Family', 'My School', 'My Neighborhood', 'Community Helpers',
'Good Manners', 'Healthy Eating', 'Personal Hygiene', 'Seasons of the Year',
'Weather Today', 'Animals on the Farm', 'Wild Animals', 'Pets and Care',
'Parts of a Plant', 'Uses of Water', 'Saving Water', 'The Sun and the Moon',
'Our Five Senses', 'Road Safety', 'Recycling Basics', 'Teamwork in Class'
],
medium: [
'Water Cycle', 'Photosynthesis', 'Solar System', 'States of Matter',
'Simple Machines', 'Electricity Basics', 'Magnetism Basics',
'Human Digestive System', 'Circulatory System Basics',
'Food Chain and Web', 'Ecosystems', 'Renewable Energy',
'Weather and Climate', 'Volcanoes and Earthquakes',
'Map Skills and Symbols', 'Ancient Egypt', 'Indus Valley Civilization',
'Indian Constitution (Basics)', 'Cyber Safety for Kids',
'Time Management for Students'
],
hard: [
'Global Warming and Climate Change', 'Greenhouse Effect', 'Plate Tectonics',
'Genetic Inheritance (Basics)', 'Natural Selection (Basics)',
'Nervous System Overview', 'Robotics in Daily Life',
'Artificial Intelligence (Basics)', 'Computer Networks (Basics)',
'Data Privacy and Security', 'Internet and Web Architecture (Basics)',
'World War II (Overview)', 'Industrial Revolution',
'Democracy and Rights (India)', 'Financial Literacy: Budgeting',
'Statistics in Daily Life (Mean, Median)',
'Renewable vs Non-renewable Energy', 'Entrepreneurship Basics',
'Ethics in Technology', 'Career Planning and Growth Mindset'
]
};
constructor(
private readingService: ReadingService,
private router: Router,
private el: ElementRef
) { }
goToIntroSection(): void {
this.stopReadAloud?.();
this.content = '';
this.hasStarted = false;
this.isGenerateDisabled = false;
this.refreshGenerateQuestionsState();
this.showCongrats = false;
}
generateContent(): void {
this.showPopup = false;
this.errorMessage = '';
if (!this.topic.trim() || !this.difficulty.trim()) {
this.errorMessage = 'Please enter a topic and select a difficulty level.';
this.showPopup = true;
return;
}
this.isGenerateDisabled = true;
this.isGeneratingContent = true;
this.readingService.generateContent(this.topic, this.difficulty).subscribe(
(response) => {
const text = (response?.content || '').trim();
if (!text) {
this.errorMessage = 'The server did not return any content.';
this.showPopup = true;
this.isGeneratingContent = false;
this.isGenerateDisabled = false;
this.content = '';
this.normalizedTopic = '';
return;
}
this.content = text;
this.normalizedTopic = (response?.normalized_topic || response?.topic || this.topic || '').trim();
this.isGeneratingContent = false;
this.hasStarted = false;
this.refreshGenerateQuestionsState();
},
(error) => {
console.error(error);
const msg = error?.error?.error || 'Invalid topic. Please enter a meaningful topic.';
this.errorMessage = msg;
this.showPopup = true;
this.isGeneratingContent = false;
this.isGenerateDisabled = false;
}
);
}
generateQuestions(): void {
if (!this.content.trim()) return;
this.loadingQuestions = true;
this.isGenerateQuestionDisabled = true;
this.readingService.generateQuestions(this.content, this.difficulty).subscribe(
(response) => {
this.questions = this.parseQuestions(response.questions);
this.selectedAnswers = {};
this.currentQuestionIndex = 0;
this.questions.forEach(q => (q.isChecked = false));
this.hasStarted = true;
this.loadingQuestions = false;
},
(error) => {
console.error(error);
this.loadingQuestions = false;
this.refreshGenerateQuestionsState();
}
);
}
parseQuestions(
raw: any
): { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] {
if (Array.isArray(raw)) {
return raw
.map((q: any) => ({
question: String(q?.question || '').trim(),
options: Array.isArray(q?.options) ? q.options.map((o: any) => String(o)) : [],
correct_answer: String(q?.correct_answer || '').trim(),
isChecked: false
}))
.filter(q => q.question && q.options.length === 4);
}
const out: { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] = [];
const text = String(raw || '');
const blocks = text.split('\n\n');
blocks.forEach(block => {
const obj: any = {};
const lines = block.split('\n');
obj.question = (lines[0] || '').replace(/^(\d+\.\s*)?Question:\s*/i, '').trim();
const optionsText = (lines[1] || '')
.replace(/^Options:\s*\[/i, '')
.replace(/\]\s*$/, '')
.trim();
obj.options = optionsText
? optionsText.split(', ').map(o => o.trim()).slice(0, 4)
: [];
obj.correct_answer = (lines[2] || '').replace(/^Correct Answer:\s*/i, '').trim();
obj.isChecked = false;
if (obj.question && obj.options?.length) out.push(obj);
});
return out;
}
setSelectedAnswer(value: string): void {
const curr = this.questions?.[this.currentQuestionIndex];
if (curr) this.selectedAnswers[curr.question] = value;
}
getSelectedAnswer(): string {
const curr = this.questions?.[this.currentQuestionIndex];
return (curr && this.selectedAnswers[curr.question]) || '';
}
nextQuestion(): void {
if (this.currentQuestionIndex < this.questions.length - 1) this.currentQuestionIndex++;
}
previousQuestion(): void {
if (this.currentQuestionIndex > 0) {
this.currentQuestionIndex--;
}
}
private markCurrentAsChecked(): void {
const curr = this.questions[this.currentQuestionIndex];
if (curr && this.selectedAnswers[curr.question]) {
curr.isChecked = true;
}
}
closeErrorPopup(): void { this.showPopup = false; }
goToHome(): void { this.router.navigate(['/home']); }
goToContentBlock(): void {
this.hasStarted = false;
this.content = '';
this.isGenerateDisabled = false;
}
resetAll(): void {
this.initState();
this.stopReadAloud?.();
this.topic = '';
this.difficulty = 'easy';
this.normalizedTopic = '';
this.content = '';
this.questions = [];
this.currentQuestionIndex = 0;
this.selectedAnswers = {};
this.hasStarted = false;
this.showCongrats = false;
this.isGenerateDisabled = false;
this.isGenerateQuestionDisabled = true;
this.isGeneratingContent = false;
this.loadingQuestions = false;
this.showPopup = false;
this.errorMessage = '';
this.scoreCorrect = 0;
this.scoreTotal = 0;
}
goBack(): void {
this.hasStarted = false;
this.refreshGenerateQuestionsState();
}
openSuggestions(): void {
this.filterSuggestions();
this.showSuggestions = true;
this.activeIndex = -1;
}
onTyping(): void {
this.filterSuggestions();
this.showSuggestions = true;
this.activeIndex = -1;
}
onKeydown(event: KeyboardEvent): void {
if (!this.showSuggestions || !this.filteredSuggestions.length) return;
if (event.key === 'ArrowDown') {
event.preventDefault();
this.activeIndex = Math.min(this.activeIndex + 1, this.filteredSuggestions.length - 1);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
this.activeIndex = Math.max(this.activeIndex - 1, 0);
} else if (event.key === 'Enter') {
event.preventDefault();
const pick = this.filteredSuggestions[this.activeIndex] || this.topic;
this.selectSuggestion(pick);
} else if (event.key === 'Escape') {
this.showSuggestions = false;
}
}
selectSuggestion(val: string): void {
this.topic = val || '';
this.showSuggestions = false;
this.isGenerateDisabled = false;
}
hideSuggestionsWithDelay(): void { setTimeout(() => (this.showSuggestions = false), 120); }
private filterSuggestions(): void {
const q = (this.topic || '').toLowerCase().trim();
const pool = this.topicsByDifficulty[this.difficulty || 'medium'] || [];
this.filteredSuggestions = q ? pool.filter(t => t.toLowerCase().includes(q)).slice(0, 12)
: pool.slice(0, 12);
}
onClearTopic(): void {
this.topic = '';
this.errorMessage = '';
this.activeIndex = -1;
this.showSuggestions = true;
this.isGenerateDisabled = false;
}
onDifficultyChange(val: string): void {
this.difficulty = (val || '') as any;
if (!this.difficulty) {
this.filteredSuggestions = [];
this.showSuggestions = false;
this.activeIndex = -1;
return;
}
this.filterSuggestions();
this.showSuggestions = true;
this.activeIndex = -1;
}
applyFont(): void {
document.documentElement.style.setProperty('--passage-font', `${this.fontPx}px`);
localStorage.setItem('passageFontPx', String(this.fontPx));
}
decreaseFont(): void { this.fontPx = Math.max(14, this.fontPx - 2); this.applyFont(); }
increaseFont(): void { this.fontPx = Math.min(30, this.fontPx + 2); this.applyFont(); }
private initState(): void {
this.hasStarted = false;
this.isGeneratingContent = false;
this.isGenerateDisabled = false;
this.isGenerateQuestionDisabled = false;
this.showPopup = false;
this.errorMessage = '';
this.topic = '';
this.normalizedTopic = '';
this.difficulty = 'easy';
this.showSuggestions = false;
this.activeIndex = -1;
this.filteredSuggestions = [];
this.content = '';
this.questions = [];
this.currentQuestionIndex = 0;
this.selectedAnswers = {};
this.fontPx = parseInt(localStorage.getItem('passageFontPx') || '18', 10);
this.applyFont();
}
ngOnInit(): void { this.initState(); }
transformContent(raw: string): string {
try {
const passage = this.extractPassage(raw);
const safe = this.escapeHtml(passage).replace(/\n+/g, ' ').trim();
return `<p>${safe}</p>`;
} catch {
return this.escapeHtml(raw || '');
}
}
private extractPassage(raw: string): string {
if (!raw) return '';
const p = raw.indexOf('Passage:');
if (p === -1) return raw;
const s = raw.indexOf('Summary:', p + 8);
const slice = s === -1 ? raw.slice(p + 8) : raw.slice(p + 8, s);
return slice.trim();
}
private escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
toggleReadAloud(): void {
if (!this.content || typeof window === 'undefined' || !('speechSynthesis' in window)) {
this.errorMessage = 'Read Aloud is not supported in this browser.';
this.showPopup = true;
return;
}
const synth = window.speechSynthesis;
if (this.isReading && !synth.paused) { synth.pause(); this.isReading = false; this.ttsPaused = true; return; }
if (this.ttsPaused) { synth.resume(); this.isReading = true; this.ttsPaused = false; return; }
this.stopReadAloud();
const text = this.extractPassage(this.content) || this.content;
const u = new SpeechSynthesisUtterance(text);
u.rate = 1.0; u.pitch = 1.0; u.lang = 'en-US';
u.onend = () => { this.isReading = false; this.ttsPaused = false; this.ttsUtterance = undefined; };
u.onerror = () => { this.isReading = false; this.ttsPaused = false; this.ttsUtterance = undefined; };
this.ttsUtterance = u;
synth.speak(u);
this.isReading = true;
}
stopReadAloud(): void {
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
window.speechSynthesis.cancel();
}
this.isReading = false;
this.ttsPaused = false;
this.ttsUtterance = undefined;
}
showCongrats = false;
scoreCorrect = 0;
private confettiInterval: any = null;
scoreTotal: number = 3;
congratsTitle: string = '';
congratsMessage: string = '';
scheduleCongratsIfLast(): void {
const isLast = this.currentQuestionIndex === this.questions.length - 1;
const curr = this.questions[this.currentQuestionIndex];
if (!isLast || !curr?.isChecked) return;
setTimeout(() => {
const { correct, total } = this.computeScore();
this.scoreCorrect = correct;
this.scoreTotal = total;
if (this.scoreCorrect === this.scoreTotal) {
this.showCongratsMessage('🎉 Congratulations 🎉', 'You have completed all questions with a perfect score!');
} else if (this.scoreCorrect > 0) {
this.showCongratsMessage('Good Job!', 'You did well, but there is room for improvement.');
} else {
this.showCongratsMessage('Keep Trying!', "Don't give up, try again!");
}
this.showCongrats = true;
this.triggerConfetti();
}, 800);
}
showCongratsMessage(title: string, message: string) {
this.congratsTitle = title;
this.congratsMessage = message;
}
private computeScore(): { correct: number; total: number } {
let correct = 0;
const total = this.questions.length;
for (const q of this.questions) {
const chosen = this.selectedAnswers[q.question];
if (chosen && chosen === q.correct_answer) correct++;
}
return { correct, total };
}
private 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 },
});
}, 250);
}
private stopConfetti(): void {
if (this.confettiInterval) {
clearInterval(this.confettiInterval);
this.confettiInterval = null;
}
const canvas = document.querySelector('canvas.confetti-canvas');
if (canvas) canvas.remove();
}
startOver(): void {
this.stopConfetti();
this.showCongrats = false;
this.resetAll();
}
ngOnDestroy(): void {
this.stopConfetti();
}
isValidating = false;
validateAnswer(): void {
this.isValidating = true;
setTimeout(() => (this.isValidating = false), 300);
const curr = this.questions?.[this.currentQuestionIndex];
if (curr && this.selectedAnswers?.[curr.question]) {
curr.isChecked = true;
}
this.scheduleCongratsIfLast();
}
private refreshGenerateQuestionsState(): void {
this.isGenerateQuestionDisabled = !(this.content && this.content.trim().length > 0);
}
goToReadingPassage(): void {
this.hasStarted = false;
this.refreshGenerateQuestionsState();
}
}