import { Component, ElementRef, ViewChild } from '@angular/core'; import { ReadingService } from './reading.service'; import { Router } from '@angular/router'; import confetti from 'canvas-confetti'; @Component({ selector: 'app-reading', templateUrl: './reading.component.html', styleUrl: './reading.component.css' }) export class ReadingComponent { // basic state loadingQuestions = false; isGeneratingContent = false; isGenerateDisabled = false; isGenerateQuestionDisabled = false; showPopup = false; showSuggestions = false; // quiz state hasStarted = false; currentQuestionIndex = 0; questions: { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] = []; selectedAnswers: { [key: string]: string } = {}; // content state topic: string = ''; difficulty: 'easy' | 'medium' | 'hard' = 'easy'; content: string = ''; errorMessage: string = ''; normalizedTopic: string = ''; // suggestions state filteredSuggestions: string[] = []; activeIndex = -1; // read-aloud + font fontPx = parseInt(localStorage.getItem('passageFontPx') || '18', 10); isReading = false; ttsPaused = false; private ttsUtterance?: SpeechSynthesisUtterance; // topic lists 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?.(); // Show Intro by clearing passage, but DO NOT touch topic/difficulty this.content = ''; this.hasStarted = false; // On Intro, allow user to click "Generate Passage" again this.isGenerateDisabled = false; // No passage now → disable "Generate Questions" this.refreshGenerateQuestionsState(); // Make sure any congrats overlay is closed 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(); // ✅ Do not check for exact topic substring 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; // Support both shapes: { normalized_topic } or { topic } or fallback to input this.normalizedTopic = (response?.normalized_topic || response?.topic || this.topic || '').trim(); this.isGeneratingContent = false; // Show passage screen; enable "Generate Questions" this.hasStarted = false; this.refreshGenerateQuestionsState(); }, (error) => { console.error(error); // ✅ Show backend message if present (your Flask returns {"error": "..."} on 400) const msg = error?.error?.error || 'Invalid topic. Please enter a meaningful topic.'; this.errorMessage = msg; this.showPopup = true; this.isGeneratingContent = false; this.isGenerateDisabled = false; } ); } /** Generate MCQs from current passage. */ 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; // IMPORTANT: allow user to try again on failure this.refreshGenerateQuestionsState(); // passage still exists → re-enable button } ); } /** Parse questions from either JSON (preferred) or old plain-text format. */ parseQuestions( raw: any ): { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] { // 1) If backend returns an array of question objects (JSON) 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); } // 2) Fallback: parse the old plain-text format 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()) : []; 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; } /** Record the selected answer for current question. */ setSelectedAnswer(value: string): void { const curr = this.questions?.[this.currentQuestionIndex]; if (curr) this.selectedAnswers[curr.question] = value; } /** Get the selected answer for current question. */ getSelectedAnswer(): string { const curr = this.questions?.[this.currentQuestionIndex]; return (curr && this.selectedAnswers[curr.question]) || ''; } /** Move to next question. */ nextQuestion(): void { if (this.currentQuestionIndex < this.questions.length - 1) this.currentQuestionIndex++; } /** Instantly marks current question as checked if an answer is chosen. */ private markCurrentAsChecked(): void { const curr = this.questions[this.currentQuestionIndex]; if (curr && this.selectedAnswers[curr.question]) { curr.isChecked = true; } } /** Close the error popup. */ closeErrorPopup(): void { this.showPopup = false; } /** Navigate to home route. */ goToHome(): void { this.router.navigate(['/home']); } /** Return to content stage and re-enable input. */ goToContentBlock(): void { this.hasStarted = false; this.content = ''; this.isGenerateDisabled = false; } /** Reset everything to initial state. */ resetAll(): void { this.initState(); this.stopReadAloud?.(); // Inputs / choices this.topic = ''; this.difficulty = 'easy'; this.normalizedTopic = ''; // Generated passage and questions this.content = ''; this.questions = []; this.currentQuestionIndex = 0; this.selectedAnswers = {}; // View flags this.hasStarted = false; this.showCongrats = false; // UX flags this.isGenerateDisabled = false; // allow new passage generation this.isGenerateQuestionDisabled = true; // disabled until a passage exists this.isGeneratingContent = false; this.loadingQuestions = false; this.showPopup = false; this.errorMessage = ''; // Score this.scoreCorrect = 0; this.scoreTotal = 0; } /** Return from MCQ to passage view. */ goBack(): void { this.hasStarted = false; // return to passage view this.refreshGenerateQuestionsState(); // passage exists → enable button } /** Open suggestions for current difficulty. */ openSuggestions(): void { this.filterSuggestions(); this.showSuggestions = true; this.activeIndex = -1; } /** Update suggestions on typing. */ onTyping(): void { this.filterSuggestions(); this.showSuggestions = true; this.activeIndex = -1; } /** Handle suggestion keyboard navigation. */ 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; } } /** Choose a suggestion and close list. */ selectSuggestion(val: string): void { this.topic = val || ''; this.showSuggestions = false; this.isGenerateDisabled = false; } /** Hide suggestions after blur. */ hideSuggestionsWithDelay(): void { setTimeout(() => (this.showSuggestions = false), 120); } /** Internal: filter topics based on query+level. */ 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); } /** Clear topic and reopen suggestions. */ onClearTopic(): void { this.topic = ''; this.errorMessage = ''; this.activeIndex = -1; this.showSuggestions = true; this.isGenerateDisabled = false; } /** Change difficulty and refresh suggestions. */ 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; } /** Apply font size CSS variable. */ applyFont(): void { document.documentElement.style.setProperty('--passage-font', `${this.fontPx}px`); localStorage.setItem('passageFontPx', String(this.fontPx)); } /** Decrease font size (min 14px). */ decreaseFont(): void { this.fontPx = Math.max(14, this.fontPx - 2); this.applyFont(); } /** Increase font size (max 30px). */ increaseFont(): void { this.fontPx = Math.min(30, this.fontPx + 2); this.applyFont(); } /** Init defaults for the screen. */ 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(); } /** Lifecycle hook to set initial UI. */ ngOnInit(): void { this.initState(); } /** Build safe HTML for passage body. */ transformContent(raw: string): string { try { const passage = this.extractPassage(raw); const safe = this.escapeHtml(passage).replace(/\n+/g, ' ').trim(); return `
${safe}
`; } catch { return this.escapeHtml(raw || ''); } } /** Extract text between "Passage:" and "Summary:". */ 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(); } /** Minimal HTML escape. */ private escapeHtml(s: string): string { return s.replace(/&/g, '&').replace(//g, '>'); } /** Toggle speech synthesis for the passage. */ 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(); // fresh start 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; } /** Stop speech and clear flags. */ stopReadAloud(): void { if (typeof window !== 'undefined' && 'speechSynthesis' in window) { window.speechSynthesis.cancel(); } this.isReading = false; this.ttsPaused = false; this.ttsUtterance = undefined; } // add inside the component class showCongrats = false; scoreCorrect = 0; private confettiInterval: any = null; // scoreCorrect: number = 0; // Correct answers scoreTotal: number = 3; // Total number of questions congratsTitle: string = ''; congratsMessage: string = ''; scheduleCongratsIfLast(): void { // must be last question AND already marked as checked const isLast = this.currentQuestionIndex === this.questions.length - 1; const curr = this.questions[this.currentQuestionIndex]; if (!isLast || !curr?.isChecked) return; // small delay so learners can see the final green/red states first setTimeout(() => { const { correct, total } = this.computeScore(); this.scoreCorrect = correct; this.scoreTotal = total; // Determine message based on score 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(); // Assuming this is a method that triggers confetti animation. }, 800); } // Helper function to show the appropriate congratulatory message showCongratsMessage(title: string, message: string) { this.congratsTitle = title; this.congratsMessage = message; } /** Compute score without altering validation code. */ private computeScore(): { correct: number; total: number } { let correct = 0; const total = this.questions.length; // Uses your existing structures: selectedAnswers[...] and question.correct_answer 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(); // use your current reset method } ngOnDestroy(): void { this.stopConfetti(); } // optional: if you want a short “Validating…” state on the button isValidating = false; /** Called by the template. Keeps existing validation, then schedules the final modal if last. */ validateAnswer(): void { // OPTIONAL: short “validating” hold on the button (remove if not needed) this.isValidating = true; setTimeout(() => (this.isValidating = false), 300); // 👉 IMPORTANT: // If you already have a function that validates/marks the current question, call it here. // Example: // this.checkAnswer(); // or // this.markCurrentAsChecked(); // or your own method // If you do not have one, the fallback below will simply mark the current question as checked. const curr = this.questions?.[this.currentQuestionIndex]; if (curr && this.selectedAnswers?.[curr.question]) { curr.isChecked = true; // fallback mark; harmless if your own method did it already } // After the question has been marked, if it is the last, open the modal this.scheduleCongratsIfLast(); } /** Enable/disable "Generate Questions" based on whether a passage exists */ private refreshGenerateQuestionsState(): void { this.isGenerateQuestionDisabled = !(this.content && this.content.trim().length > 0); } }