import React, { useState, useEffect, useRef, useCallback } from 'react';
import { BookOpen, CheckCircle, XCircle, AlertCircle, Loader2, Award, Play, Settings, RefreshCw, ChevronRight, Brain, Home, GraduationCap, Scroll, Trophy, UserCircle, LogIn, LayoutGrid, Globe, Volume2, VolumeX, Mic, MicOff, MessageSquare, Send, X, Bot, Plus, Lock, Unlock, FileText, Download, Camera, Image as ImageIcon, Zap, Sparkles, StickyNote, PenTool, RotateCw, ChevronLeft, Star, Users, ArrowRight, Upload, PieChart, Activity, Timer, Edit3, Lightbulb, Pause, RotateCcw, Save, FileType, File, Layers, AlignLeft, List, LogOut, Phone, Mail, ShieldCheck, Trash2, Languages } from 'lucide-react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged, signOut, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile, deleteUser } from 'firebase/auth';
import { getFirestore, doc, setDoc, getDoc, collection, onSnapshot, updateDoc, increment, set, arrayUnion, deleteDoc, getDocs } from 'firebase/firestore';
// --- Firebase Configuration ---
const firebaseConfig = JSON.parse(__firebase_config);
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
// --- Backend (Stripe) Configuration ---
// Set this to your backend base URL (Express) OR Firebase Functions base.
// Example (Functions): https://us-central1-YOUR_PROJECT.cloudfunctions.net
// Example (Express): https://api.yourdomain.com
const BACKEND_BASE_URL = "";
// Stripe Price ID for the All-Access subscription (e.g., price_123...)
const STRIPE_ALL_ACCESS_PRICE_ID = "";
// When true, subscription activation should go through Stripe Checkout (recommended for live).
// When false, the app can still use the in-app "demo activation" (useful during development).
const USE_STRIPE_BILLING = true;
// --- Gemini API Helper ---
const apiKey = ""; // System provides this at runtime
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const callGeminiAPI = async (prompt, retries = 5, isJson = true, imageBase64 = null) => {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
const parts = [{ text: prompt }];
if (imageBase64) {
parts.push({
inlineData: {
mimeType: "image/jpeg",
data: imageBase64
}
});
}
const payload = {
contents: [{ parts: parts }],
generationConfig: {
responseMimeType: isJson ? "application/json" : "text/plain"
}
};
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
let text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) throw new Error("No content generated");
if (isJson) {
text = text.replace(/```json/g, '').replace(/```/g, '');
const firstArr = text.indexOf('[');
const firstObj = text.indexOf('{');
let firstIndex = -1;
let lastIndex = -1;
if (firstArr !== -1 && (firstObj === -1 || firstArr < firstObj)) {
firstIndex = firstArr;
lastIndex = text.lastIndexOf(']');
} else if (firstObj !== -1) {
firstIndex = firstObj;
lastIndex = text.lastIndexOf('}');
}
if (firstIndex !== -1 && lastIndex !== -1) {
text = text.substring(firstIndex, lastIndex + 1);
}
try {
return JSON.parse(text);
} catch (parseError) {
console.error("JSON Parse Error:", parseError, text);
throw new Error("Invalid JSON format");
}
}
return text;
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error);
if (i === retries - 1) throw error;
await delay(Math.pow(2, i) * 1000);
}
}
};
// --- Data & Constants ---
const EXAM_DATA = {
'ENEM': { name: 'ENEM', region: 'Brazil', subjects: ['Matemática', 'Ciências da Natureza', 'Ciências Humanas', 'Linguagens'] },
'SAT': { name: 'SAT', region: 'USA', subjects: ['Math', 'Reading', 'Writing'] },
'GCSE': { name: 'GCSE', region: 'UK', subjects: ['Mathematics', 'English Language', 'Combined Science', 'History', 'Geography'] },
'BAC': { name: 'Baccalauréat', region: 'France', subjects: ['Mathématiques', 'Français', 'Histoire-Géographie', 'Philosophie', 'Physique-Chimie'] },
'EBAU': { name: 'EBAU (Selectividad)', region: 'Spain', subjects: ['Matemáticas', 'Lengua Castellana', 'Historia de España', 'Inglés', 'Física'] },
'MATURITA': { name: 'Maturità', region: 'Italy', subjects: ['Italiano', 'Matematica', 'Storia', 'Inglese', 'Fisica'] },
'ABITUR': { name: 'Abitur', region: 'Germany', subjects: ['Mathematik', 'Deutsch', 'Englisch', 'Geschichte', 'Biologie'] },
};
const LANGUAGE_NAMES = {
en: "English",
pt: "Portuguese (Brazil)",
fr: "French",
it: "Italian",
de: "German",
es: "Spanish"
};
const TRANSLATIONS = {
en: {
appTitle: "TheSo Smart Learning",
tagline: "Your Personal AI-Powered Tutor",
loading: "Generating Content...",
loadingLesson: "Writing your lesson...",
loadingAnalyze: "Analyzing...",
loadingGrading: "Grading Essay...",
startQuiz: "Start Quiz",
next: "Next",
finish: "Finish",
results: "Results",
score: "Score",
pathTitle: "Learning Path",
createPath: "Create New Learning Path",
enterSubject: "What do you want to learn?",
subjectPlaceholder: "e.g., Container Ships, Quantum Physics...",
generatePath: "Generate Path",
modules: "Modules",
lesson: "Lesson",
startLessonQuiz: "Take Topic Quiz",
addTopic: "Add Topic",
certificate: "Certificate of Completion",
passRequirement: "You need 80% to pass this module.",
moduleFailed: "Module Failed. Review the lesson and try again.",
modulePassed: "Module Passed! Next topic unlocked.",
aiHelperHint: "Click the bot icon for help.",
quickExplainTitle: "Quick Explanation",
quickExplainDesc: "Instant explanation for any topic",
photoSolverTitle: "Smart Solver",
photoSolverDesc: "Upload or paste a problem to get a solution",
enterTopic: "Enter a topic or paste a problem...",
explainBtn: "Get Explanation",
uploadImage: "Upload Image",
analyzeImage: "Analyze Image",
solution: "Solution / Explanation",
flashcardsTitle: "AI Flashcards",
flashcardsDesc: "Generate study cards for any topic",
generateCards: "Generate Cards",
flipCard: "Click to Flip",
essayGraderTitle: "Essay Grader",
essayGraderDesc: "Get scores & feedback on your writing",
essayTopicPlaceholder: "Essay Topic / Title",
essayPlaceholder: "Type or paste your essay here...",
gradeEssay: "Grade My Essay",
essayFeedback: "Feedback",
improvements: "Improvements",
aiTutor: "AI Tutor",
chatWelcome: "Hi! I'm here to help you learn.",
chatPlaceholder: "Ask me anything...",
explanation: "Explanation",
profileTitle: "Student Profile",
activePaths: "Active Courses",
completedModules: "Completed Topics",
avgGrade: "Average Grade",
quizGenTitle: "Quiz from Content",
quizGenDesc: "Upload notes or paste text to generate a quiz",
pasteContent: "Paste your lesson content or question here...",
generateQuizBtn: "Generate Quiz",
smartSolverTitle: "Smart Solver",
solveBtn: "Solve & Explain",
focusModeTitle: "Focus Timer",
focusModeDesc: "Boost productivity with Pomodoro",
notesTitle: "Smart Notes",
dailyDiscovery: "Daily Discovery",
startFocus: "Start Focus",
stopFocus: "Stop",
quizTopicTitle: "Quiz from Topic",
quizTopicDesc: "Create a custom quiz on any subject",
questionCount: "Number of Questions",
startCustomQuiz: "Start Custom Quiz",
summarizerTitle: "Smart Summarizer",
summarizerDesc: "Condense long text into key points",
mnemonicTitle: "Mnemonic Master",
mnemonicDesc: "Create memory aids for lists & facts",
summarizeBtn: "Summarize Text",
generateMnemonicBtn: "Create Memory Aid",
pasteTextToSummarize: "Paste text or upload file to summarize...",
pasteItemsToMemorize: "Enter list of items to memorize...",
uploadFile: "Upload File",
tools: "Tools",
profile: "Profile",
login: "Login / Register",
online: "Online",
logout: "Sign Out",
deleteAccount: "Delete Account",
back: "Back",
home: "Home",
prepareExams: "Prepare for Exams",
startLearning: "Start Learning"
},
pt: {
appTitle: "TheSo Smart Learning",
tagline: "Seu Tutor Pessoal com IA",
loading: "Gerando Conteúdo...",
loadingLesson: "Escrevendo sua lição...",
loadingAnalyze: "Analisando...",
loadingGrading: "Avaliando Redação...",
startQuiz: "Iniciar Quiz",
next: "Próximo",
finish: "Finalizar",
results: "Resultados",
score: "Pontuação",
pathTitle: "Trilha de Aprendizagem",
createPath: "Criar Nova Trilha",
enterSubject: "O que você quer aprender?",
subjectPlaceholder: "ex: Navios, Física Quântica...",
generatePath: "Gerar Trilha",
modules: "Módulos",
lesson: "Lição",
startLessonQuiz: "Fazer Quiz",
addTopic: "Adicionar Tópico",
certificate: "Certificado de Conclusão",
passRequirement: "Você precisa de 80% para passar.",
moduleFailed: "Reprovado. Revise e tente novamente.",
modulePassed: "Aprovado! Próximo tópico desbloqueado.",
aiHelperHint: "Clique no robô para ajuda.",
quickExplainTitle: "Explicação Rápida",
quickExplainDesc: "Explicação instantânea para qualquer tópico",
photoSolverTitle: "Resolvedor Inteligente",
photoSolverDesc: "Envie ou cole um problema para resolver",
enterTopic: "Digite um tópico ou cole um problema...",
explainBtn: "Obter Explicação",
uploadImage: "Enviar Imagem",
analyzeImage: "Analisar Imagem",
solution: "Solução / Explicação",
flashcardsTitle: "Flashcards IA",
flashcardsDesc: "Gere cartões de estudo",
generateCards: "Gerar Cartões",
flipCard: "Clique para Virar",
essayGraderTitle: "Corretor de Redação",
essayGraderDesc: "Receba notas e feedback",
essayTopicPlaceholder: "Título da Redação",
essayPlaceholder: "Digite sua redação aqui...",
gradeEssay: "Avaliar Minha Redação",
essayFeedback: "Feedback",
improvements: "Melhorias",
aiTutor: "Tutor IA",
chatWelcome: "Oi! Estou aqui para ajudar.",
chatPlaceholder: "Pergunte qualquer coisa...",
explanation: "Explicação",
profileTitle: "Perfil do Estudante",
activePaths: "Cursos Ativos",
completedModules: "Tópicos Concluídos",
avgGrade: "Média",
quizGenTitle: "Quiz de Conteúdo",
quizGenDesc: "Gere um quiz de suas notas",
pasteContent: "Cole o conteúdo aqui...",
generateQuizBtn: "Gerar Quiz",
smartSolverTitle: "Resolvedor Inteligente",
solveBtn: "Resolver e Explicar",
focusModeTitle: "Timer de Foco",
focusModeDesc: "Aumente a produtividade",
notesTitle: "Notas Inteligentes",
dailyDiscovery: "Descoberta Diária",
startFocus: "Iniciar",
stopFocus: "Parar",
quizTopicTitle: "Quiz por Tópico",
quizTopicDesc: "Crie um quiz personalizado",
questionCount: "Número de Perguntas",
startCustomQuiz: "Iniciar Quiz",
summarizerTitle: "Resumidor Inteligente",
summarizerDesc: "Resuma textos longos",
mnemonicTitle: "Mestre Mnemônico",
mnemonicDesc: "Crie ajudas de memória",
summarizeBtn: "Resumir Texto",
generateMnemonicBtn: "Criar Mnemônico",
pasteTextToSummarize: "Cole o texto aqui...",
pasteItemsToMemorize: "Liste os itens...",
uploadFile: "Enviar Arquivo",
tools: "Ferramentas",
profile: "Perfil",
login: "Entrar / Registrar",
online: "Online",
logout: "Sair",
deleteAccount: "Excluir Conta",
back: "Voltar",
home: "Início",
prepareExams: "Preparar para Exames",
startLearning: "Começar a Aprender"
},
fr: {
appTitle: "TheSo Smart Learning",
tagline: "Votre tuteur personnel IA",
loading: "Génération de contenu...",
loadingLesson: "Rédaction de la leçon...",
loadingAnalyze: "Analyse en cours...",
loadingGrading: "Correction de l'essai...",
startQuiz: "Commencer le Quiz",
next: "Suivant",
finish: "Terminer",
results: "Résultats",
score: "Score",
pathTitle: "Parcours d'apprentissage",
createPath: "Créer un parcours",
enterSubject: "Que voulez-vous apprendre ?",
subjectPlaceholder: "ex: Physique quantique...",
generatePath: "Générer le parcours",
modules: "Modules",
lesson: "Leçon",
startLessonQuiz: "Quiz du sujet",
addTopic: "Ajouter un sujet",
certificate: "Certificat d'achèvement",
passRequirement: "Il faut 80% pour réussir.",
moduleFailed: "Échec. Révisez et réessayez.",
modulePassed: "Réussi ! Sujet suivant débloqué.",
aiHelperHint: "Cliquez sur le bot pour de l'aide.",
quickExplainTitle: "Explication rapide",
quickExplainDesc: "Explication instantanée",
photoSolverTitle: "Résolveur intelligent",
photoSolverDesc: "Analysez un problème",
enterTopic: "Entrez un sujet...",
explainBtn: "Obtenir l'explication",
uploadImage: "Télécharger une image",
analyzeImage: "Analyser l'image",
solution: "Solution / Explication",
flashcardsTitle: "Flashcards IA",
flashcardsDesc: "Générez des cartes d'étude",
generateCards: "Générer des cartes",
flipCard: "Cliquez pour retourner",
essayGraderTitle: "Correcteur d'essai",
essayGraderDesc: "Obtenez une note et des conseils",
essayTopicPlaceholder: "Sujet de l'essai",
essayPlaceholder: "Tapez votre essai ici...",
gradeEssay: "Noter mon essai",
essayFeedback: "Commentaires",
improvements: "Améliorations",
aiTutor: "Tutor IA",
chatWelcome: "Salut ! Je suis là pour aider.",
chatPlaceholder: "Demandez-moi n'importe quoi...",
explanation: "Explication",
profileTitle: "Profil étudiant",
activePaths: "Cours actifs",
completedModules: "Sujets terminés",
avgGrade: "Moyenne",
quizGenTitle: "Quiz de contenu",
quizGenDesc: "Quiz à partir de vos notes",
pasteContent: "Collez le contenu ici...",
generateQuizBtn: "Générer le Quiz",
smartSolverTitle: "Résolveur intelligent",
solveBtn: "Résoudre et expliquer",
focusModeTitle: "Minuteur de concentration",
focusModeDesc: "Boostez votre productivité",
notesTitle: "Notes intelligentes",
dailyDiscovery: "Découverte quotidienne",
startFocus: "Démarrer",
stopFocus: "Arrêter",
quizTopicTitle: "Quiz par sujet",
quizTopicDesc: "Créez un quiz personnalisé",
questionCount: "Nombre de questions",
startCustomQuiz: "Lancer le Quiz",
summarizerTitle: "Résumé intelligent",
summarizerDesc: "Condenser le texte",
mnemonicTitle: "Maître Mnémonique",
mnemonicDesc: "Aides à la mémoire",
summarizeBtn: "Résumer",
generateMnemonicBtn: "Créer une aide",
pasteTextToSummarize: "Collez le texte...",
pasteItemsToMemorize: "Listez les éléments...",
uploadFile: "Télécharger un fichier",
tools: "Outils",
profile: "Profil",
login: "Connexion",
online: "En ligne",
logout: "Déconnexion",
deleteAccount: "Supprimer le compte",
back: "Retour",
home: "Accueil",
prepareExams: "Préparer les examens",
startLearning: "Commencer"
},
it: {
appTitle: "TheSo Smart Learning",
tagline: "Il tuo tutor personale AI",
loading: "Generazione contenuti...",
loadingLesson: "Scrivendo la lezione...",
loadingAnalyze: "Analizzando...",
loadingGrading: "Valutando il saggio...",
startQuiz: "Inizia Quiz",
next: "Avanti",
finish: "Fine",
results: "Risultati",
score: "Punteggio",
pathTitle: "Percorso di apprendimento",
createPath: "Crea nuovo percorso",
enterSubject: "Cosa vuoi imparare?",
subjectPlaceholder: "es: Fisica quantistica...",
generatePath: "Genera percorso",
modules: "Moduli",
lesson: "Lezione",
startLessonQuiz: "Fai il Quiz",
addTopic: "Aggiungi argomento",
certificate: "Certificato di completamento",
passRequirement: "Serve l'80% per passare.",
moduleFailed: "Fallito. Ripassa e riprova.",
modulePassed: "Superato! Prossimo sbloccato.",
aiHelperHint: "Clicca sul bot per aiuto.",
quickExplainTitle: "Spiegazione rapida",
quickExplainDesc: "Spiegazione istantanea",
photoSolverTitle: "Risolutore Smart",
photoSolverDesc: "Carica un problema per la soluzione",
enterTopic: "Inserisci un argomento...",
explainBtn: "Ottieni spiegazione",
uploadImage: "Carica immagine",
analyzeImage: "Analizza immagine",
solution: "Soluzione / Spiegazione",
flashcardsTitle: "Flashcards AI",
flashcardsDesc: "Genera carte studio",
generateCards: "Genera Carte",
flipCard: "Clicca per girare",
essayGraderTitle: "Correttore saggi",
essayGraderDesc: "Ottieni voti e feedback",
essayTopicPlaceholder: "Argomento del saggio",
essayPlaceholder: "Scrivi qui il tuo saggio...",
gradeEssay: "Valuta il mio saggio",
essayFeedback: "Feedback",
improvements: "Miglioramenti",
aiTutor: "Tutor AI",
chatWelcome: "Ciao! Sono qui per aiutarti.",
chatPlaceholder: "Chiedimi qualsiasi cosa...",
explanation: "Spiegazione",
profileTitle: "Profilo studente",
activePaths: "Corsi attivi",
completedModules: "Argomenti completati",
avgGrade: "Media",
quizGenTitle: "Quiz da contenuto",
quizGenDesc: "Quiz dai tuoi appunti",
pasteContent: "Incolla il contenuto...",
generateQuizBtn: "Genera Quiz",
smartSolverTitle: "Risolutore Smart",
solveBtn: "Risolvi e spiega",
focusModeTitle: "Timer Focus",
focusModeDesc: "Migliora la produttività",
notesTitle: "Note intelligenti",
dailyDiscovery: "Scoperta giornaliera",
startFocus: "Inizia",
stopFocus: "Stop",
quizTopicTitle: "Quiz per argomento",
quizTopicDesc: "Crea un quiz personalizzato",
questionCount: "Numero di domande",
startCustomQuiz: "Avvia Quiz",
summarizerTitle: "Riassuntore Smart",
summarizerDesc: "Condensa testi lunghi",
mnemonicTitle: "Maestro Mnemonico",
mnemonicDesc: "Aiuti per la memoria",
summarizeBtn: "Riassumi testo",
generateMnemonicBtn: "Crea aiuto memoria",
pasteTextToSummarize: "Incolla il testo...",
pasteItemsToMemorize: "Elenca gli elementi...",
uploadFile: "Carica file",
tools: "Strumenti",
profile: "Profilo",
login: "Accedi / Registrati",
online: "Online",
logout: "Esci",
deleteAccount: "Elimina account",
back: "Indietro",
home: "Home",
prepareExams: "Prepara esami",
startLearning: "Inizia a imparare"
},
de: {
appTitle: "TheSo Smart Learning",
tagline: "Dein persönlicher KI-Tutor",
loading: "Inhalt wird generiert...",
loadingLesson: "Lektion wird erstellt...",
loadingAnalyze: "Analysieren...",
loadingGrading: "Aufsatz wird bewertet...",
startQuiz: "Quiz starten",
next: "Weiter",
finish: "Beenden",
results: "Ergebnisse",
score: "Punktzahl",
pathTitle: "Lernpfad",
createPath: "Neuen Pfad erstellen",
enterSubject: "Was möchtest du lernen?",
subjectPlaceholder: "z.B. Quantenphysik...",
generatePath: "Pfad generieren",
modules: "Module",
lesson: "Lektion",
startLessonQuiz: "Quiz machen",
addTopic: "Thema hinzufügen",
certificate: "Abschlusszertifikat",
passRequirement: "Du brauchst 80% zum Bestehen.",
moduleFailed: "Nicht bestanden. Wiederhole die Lektion.",
modulePassed: "Bestanden! Nächstes Thema freigeschaltet.",
aiHelperHint: "Klicke auf den Bot für Hilfe.",
quickExplainTitle: "Schnellerklärung",
quickExplainDesc: "Sofortige Erklärung",
photoSolverTitle: "Smart Solver",
photoSolverDesc: "Lade ein Problem hoch",
enterTopic: "Thema eingeben...",
explainBtn: "Erklärung erhalten",
uploadImage: "Bild hochladen",
analyzeImage: "Bild analysieren",
solution: "Lösung / Erklärung",
flashcardsTitle: "KI-Karteikarten",
flashcardsDesc: "Erstelle Lernkarten",
generateCards: "Karten generieren",
flipCard: "Zum Umdrehen klicken",
essayGraderTitle: "Aufsatz-Bewerter",
essayGraderDesc: "Noten & Feedback erhalten",
essayTopicPlaceholder: "Aufsatzthema",
essayPlaceholder: "Aufsatz hier eingeben...",
gradeEssay: "Aufsatz bewerten",
essayFeedback: "Feedback",
improvements: "Verbesserungen",
aiTutor: "KI-Tutor",
chatWelcome: "Hallo! Ich helfe dir beim Lernen.",
chatPlaceholder: "Frag mich alles...",
explanation: "Erklärung",
profileTitle: "Schülerprofil",
activePaths: "Aktive Kurse",
completedModules: "Abgeschlossene Themen",
avgGrade: "Durchschnitt",
quizGenTitle: "Quiz aus Inhalt",
quizGenDesc: "Quiz aus Notizen erstellen",
pasteContent: "Inhalt hier einfügen...",
generateQuizBtn: "Quiz generieren",
smartSolverTitle: "Smart Solver",
solveBtn: "Lösen & Erklären",
focusModeTitle: "Fokus-Timer",
focusModeDesc: "Produktivität steigern",
notesTitle: "Smarte Notizen",
dailyDiscovery: "Tägliche Entdeckung",
startFocus: "Starten",
stopFocus: "Stopp",
quizTopicTitle: "Themen-Quiz",
quizTopicDesc: "Erstelle ein eigenes Quiz",
questionCount: "Anzahl Fragen",
startCustomQuiz: "Quiz starten",
summarizerTitle: "Smarter Zusammenfasser",
summarizerDesc: "Lange Texte kürzen",
mnemonicTitle: "Mnemotechnik-Meister",
mnemonicDesc: "Gedächtnishilfen erstellen",
summarizeBtn: "Zusammenfassen",
generateMnemonicBtn: "Merkhilfe erstellen",
pasteTextToSummarize: "Text hier einfügen...",
pasteItemsToMemorize: "Liste eingeben...",
uploadFile: "Datei hochladen",
tools: "Tools",
profile: "Profil",
login: "Anmelden",
online: "Online",
logout: "Abmelden",
deleteAccount: "Konto löschen",
back: "Zurück",
home: "Startseite",
prepareExams: "Prüfungsvorbereitung",
startLearning: "Lernen starten"
},
es: {
appTitle: "TheSo Smart Learning",
tagline: "Tu Tutor Personal con IA",
loading: "Generando contenido...",
loadingLesson: "Escribiendo lección...",
loadingAnalyze: "Analizando...",
loadingGrading: "Calificando ensayo...",
startQuiz: "Iniciar Quiz",
next: "Siguiente",
finish: "Terminar",
results: "Resultados",
score: "Puntuación",
pathTitle: "Ruta de Aprendizaje",
createPath: "Crear Nueva Ruta",
enterSubject: "¿Qué quieres aprender?",
subjectPlaceholder: "ej: Física Cuántica...",
generatePath: "Generar Ruta",
modules: "Módulos",
lesson: "Lección",
startLessonQuiz: "Tomar Quiz",
addTopic: "Añadir Tema",
certificate: "Certificado de Finalización",
passRequirement: "Necesitas 80% para aprobar.",
moduleFailed: "Reprobado. Repasa y reintenta.",
modulePassed: "¡Aprobado! Siguiente tema desbloqueado.",
aiHelperHint: "Clic en el bot para ayuda.",
quickExplainTitle: "Explicación Rápida",
quickExplainDesc: "Explicación instantánea",
photoSolverTitle: "Solucionador Inteligente",
photoSolverDesc: "Sube un problema para resolver",
enterTopic: "Ingresa un tema...",
explainBtn: "Obtener Explicación",
uploadImage: "Subir Imagen",
analyzeImage: "Analizar Imagen",
solution: "Solución / Explicación",
flashcardsTitle: "Flashcards IA",
flashcardsDesc: "Genera tarjetas de estudio",
generateCards: "Generar Tarjetas",
flipCard: "Clic para Voltear",
essayGraderTitle: "Calificador de Ensayos",
essayGraderDesc: "Obtén notas y comentarios",
essayTopicPlaceholder: "Tema del ensayo",
essayPlaceholder: "Escribe tu ensayo aquí...",
gradeEssay: "Calificar Mi Ensayo",
essayFeedback: "Comentarios",
improvements: "Mejoras",
aiTutor: "Tutor IA",
chatWelcome: "¡Hola! Estoy aquí para ayudarte.",
chatPlaceholder: "Pregúntame lo que sea...",
explanation: "Explicación",
profileTitle: "Perfil de Estudiante",
activePaths: "Cursos Activos",
completedModules: "Temas Completados",
avgGrade: "Promedio",
quizGenTitle: "Quiz de Contenido",
quizGenDesc: "Quiz desde tus notas",
pasteContent: "Pega el contenido aquí...",
generateQuizBtn: "Generar Quiz",
smartSolverTitle: "Solucionador Inteligente",
solveBtn: "Resolver y Explicar",
focusModeTitle: "Temporizador de Enfoque",
focusModeDesc: "Mejora la productividad",
notesTitle: "Notas Inteligentes",
dailyDiscovery: "Descubrimiento Diario",
startFocus: "Iniciar",
stopFocus: "Parar",
quizTopicTitle: "Quiz por Tema",
quizTopicDesc: "Crea un quiz personalizado",
questionCount: "Número de Preguntas",
startCustomQuiz: "Iniciar Quiz",
summarizerTitle: "Resumidor Inteligente",
summarizerDesc: "Resume textos largos",
mnemonicTitle: "Maestro Mnemónico",
mnemonicDesc: "Ayudas para la memoria",
summarizeBtn: "Resumir Texto",
generateMnemonicBtn: "Crear Ayuda",
pasteTextToSummarize: "Pega el texto...",
pasteItemsToMemorize: "Lista los ítems...",
uploadFile: "Subir Archivo",
tools: "Herramientas",
profile: "Perfil",
login: "Entrar / Registrar",
online: "En línea",
logout: "Salir",
deleteAccount: "Borrar Cuenta",
back: "Atrás",
home: "Inicio",
prepareExams: "Preparar Exámenes",
startLearning: "Empezar a Aprender"
}
};
// --- App Components ---
export default function App() {
// Navigation & Game State
const [gameState, setGameState] = useState('menu');
const [previousGameState, setPreviousGameState] = useState(null);
const [user, setUser] = useState(null);
const [showAuthModal, setShowAuthModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showSubscribeModal, setShowSubscribeModal] = useState(false);
const [postSubscribeState, setPostSubscribeState] = useState(null);
const [userProfile, setUserProfile] = useState(null);
const [isSubscriber, setIsSubscriber] = useState(false);
const [language, setLanguage] = useState('en');
// Multi-profile (Premium only)
const [profiles, setProfiles] = useState([{ id: 'self', name: 'My Profile', isSelf: true }]);
const [activeProfileId, setActiveProfileId] = useState('self');
const [showProfilesModal, setShowProfilesModal] = useState(false);
const [profileError, setProfileError] = useState('');
// Accessibility & Chat
const [isReading, setIsReading] = useState(false);
const [isListening, setIsListening] = useState(false);
const [isChatOpen, setIsChatOpen] = useState(false);
const [chatMessages, setChatMessages] = useState([]);
const [chatInput, setChatInput] = useState('');
const [isChatLoading, setIsChatLoading] = useState(false);
// Data State
const [examProgress, setExamProgress] = useState({});
const [learningPaths, setLearningPaths] = useState([]);
const [currentPath, setCurrentPath] = useState(null);
const [currentModuleIndex, setCurrentModuleIndex] = useState(0);
const [currentLessonContent, setCurrentLessonContent] = useState('');
// Quiz State
const [questions, setQuestions] = useState([]);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [userAnswers, setUserAnswers] = useState({});
const [score, setScore] = useState(0);
const [errorMsg, setErrorMsg] = useState('');
// Selection/Input State
const [selectedExamId, setSelectedExamId] = useState(null);
const [topic, setTopic] = useState('');
const [newTopicInput, setNewTopicInput] = useState('');
const [contentInput, setContentInput] = useState('');
// New Modes State
const [explanationLevel, setExplanationLevel] = useState('Medium');
const [explanationResult, setExplanationResult] = useState('');
const [selectedImage, setSelectedImage] = useState(null);
const [customQuizTopic, setCustomQuizTopic] = useState('');
const [customQuizCount, setCustomQuizCount] = useState(5);
// Summarizer & Mnemonic State
const [summarizerInput, setSummarizerInput] = useState('');
const [summarizerResult, setSummarizerResult] = useState('');
const [mnemonicInput, setMnemonicInput] = useState('');
const [mnemonicResult, setMnemonicResult] = useState('');
// Flashcards State
const [flashcards, setFlashcards] = useState([]);
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const [isCardFlipped, setIsCardFlipped] = useState(false);
// Essay Grader State
const [essayText, setEssayText] = useState('');
const [essayResult, setEssayResult] = useState(null);
// Productivity State
const [timerSeconds, setTimerSeconds] = useState(25 * 60);
const [isTimerActive, setIsTimerActive] = useState(false);
const [notes, setNotes] = useState('');
const [dailyFact, setDailyFact] = useState('');
// Export & Notes State
const [showExportModal, setShowExportModal] = useState(false);
const [exportData, setExportData] = useState(null);
const t = (key) => TRANSLATIONS[language]?.[key] || TRANSLATIONS['en'][key];
// --- Auth & Data ---
useEffect(() => {
const initAuth = async () => {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
if (!auth.currentUser) {
await signInAnonymously(auth);
}
}
};
initAuth();
onAuthStateChanged(auth, setUser);
}, []);
useEffect(() => {
if (!user || user.isAnonymous) return;
// Multi-profile: Free users always use the main (self) learningPaths collection.
// Premium users can switch to a sub-profile collection.
const pathsRef =
isSubscriber && activeProfileId !== 'self'
? collection(db, 'artifacts', appId, 'users', user.uid, 'profiles', activeProfileId, 'learningPaths')
: collection(db, 'artifacts', appId, 'users', user.uid, 'learningPaths');
const unsubscribe = onSnapshot(pathsRef, (snapshot) => {
const paths = [];
snapshot.forEach(doc => paths.push({ id: doc.id, ...doc.data() }));
setLearningPaths(paths);
if (currentPath) {
const updated = paths.find(p => p.id === currentPath.id);
if (updated) setCurrentPath(updated);
}
});
return () => unsubscribe();
}, [user, currentPath?.id, isSubscriber, activeProfileId]);
// --- Profile (Subscription) Listener ---
useEffect(() => {
if (!user || user.isAnonymous) {
setUserProfile(null);
setIsSubscriber(false);
return;
}
const profileRef = doc(db, 'artifacts', appId, 'users', user.uid, 'settings', 'profile');
const unsubscribe = onSnapshot(profileRef, (snap) => {
if (!snap.exists()) {
setUserProfile(null);
setIsSubscriber(false);
return;
}
const data = snap.data();
setUserProfile(data);
const sub = data?.subscription;
const active = sub?.status === 'active' && sub?.plan === 'all_access';
setIsSubscriber(!!active);
});
return () => unsubscribe();
}, [user?.uid]);
// --- Profiles (Premium only) Listener ---
useEffect(() => {
// Default "self" profile (works for free users too)
const selfName =
userProfile?.name ||
user?.displayName ||
(user && !user.isAnonymous ? 'My Profile' : 'Guest');
// Guests / anonymous: single profile only
if (!user || user.isAnonymous) {
setProfiles([{ id: 'self', name: selfName, isSelf: true }]);
setActiveProfileId('self');
return;
}
// Free accounts: single profile only (no extra profiles)
if (!isSubscriber) {
setProfiles([{ id: 'self', name: selfName, isSelf: true }]);
setActiveProfileId('self');
return;
}
// Premium: load sub-profiles
const profilesRef = collection(db, 'artifacts', appId, 'users', user.uid, 'profiles');
const unsubscribe = onSnapshot(profilesRef, (snapshot) => {
const arr = [{ id: 'self', name: selfName, isSelf: true }];
snapshot.forEach((d) => {
arr.push({ id: d.id, ...d.data() });
});
// Cap to 5 total profiles including self
const capped = arr.slice(0, 5);
setProfiles(capped);
// If active profile was deleted or doesn't exist anymore, fallback to self
const stillExists = capped.some(p => p.id === activeProfileId);
if (!stillExists) setActiveProfileId('self');
});
return () => unsubscribe();
}, [user?.uid, user?.isAnonymous, userProfile?.name, user?.displayName, isSubscriber, activeProfileId]);
// --- Backend helpers (Stripe / Admin) ---
const backendPost = async (path, body) => {
if (!BACKEND_BASE_URL) throw new Error("BACKEND_BASE_URL is not configured.");
if (!user || user.isAnonymous) throw new Error("You must be logged in.");
const token = await auth.currentUser.getIdToken();
const resp = await fetch(`${BACKEND_BASE_URL}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify(body || {})
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok) throw new Error(data?.error || `Request failed (${resp.status})`);
return data;
};
const startStripeCheckout = async (navigateToAfterSuccess = null) => {
if (!USE_STRIPE_BILLING) {
// fallback (dev/demo)
await activateAllAccess(navigateToAfterSuccess);
return;
}
if (!STRIPE_ALL_ACCESS_PRICE_ID) throw new Error("STRIPE_ALL_ACCESS_PRICE_ID is not configured.");
setPostSubscribeState(navigateToAfterSuccess);
const successUrl = window.location.origin + "/?upgrade=success";
const cancelUrl = window.location.origin + "/?upgrade=cancel";
const data = await backendPost("/createCheckoutSession", {
appId,
priceId: STRIPE_ALL_ACCESS_PRICE_ID,
successUrl,
cancelUrl
});
if (data?.url) window.location.href = data.url;
};
const openBillingPortal = async () => {
const returnUrl = window.location.origin + "/?portal=return";
const data = await backendPost("/createPortalSession", { appId, returnUrl });
if (data?.url) window.location.href = data.url;
};
// Handle Stripe return URLs (success/cancel) to guide the user.
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const upgrade = params.get("upgrade");
if (upgrade === "success") {
// Webhook will set subscription status. We just show a friendly message and navigate.
params.delete("upgrade");
const newUrl = window.location.pathname + (params.toString() ? `?${params.toString()}` : "");
window.history.replaceState({}, "", newUrl);
setShowSubscribeModal(false);
if (postSubscribeState) setGameState(postSubscribeState);
} else if (upgrade === "cancel") {
params.delete("upgrade");
const newUrl = window.location.pathname + (params.toString() ? `?${params.toString()}` : "");
window.history.replaceState({}, "", newUrl);
setShowSubscribeModal(false);
}
}, [postSubscribeState]);
const activateAllAccess = async (navigateTo = null) => {
if (!user || user.isAnonymous) {
setShowAuthModal(true);
return;
}
try {
const profileRef = doc(db, 'artifacts', appId, 'users', user.uid, 'settings', 'profile');
await updateDoc(profileRef, {
subscription: {
plan: 'all_access',
status: 'active',
startDate: new Date().toISOString()
}
});
setShowSubscribeModal(false);
setPostSubscribeState(null);
if (navigateTo) setGameState(navigateTo);
} catch (e) {
console.error('Subscription activate error', e);
alert('Could not activate subscription. Please try again.');
}
};
const requireAllAccess = (targetState) => {
if (!user || user.isAnonymous) {
setShowAuthModal(true);
return;
}
if (!isSubscriber) {
setPostSubscribeState(targetState);
setShowSubscribeModal(true);
return;
}
setGameState(targetState);
};
// --- Premium Multi-Profile helpers ---
const getLearningPathsCollectionRef = (uid) => {
if (isSubscriber && activeProfileId !== 'self') {
return collection(db, 'artifacts', appId, 'users', uid, 'profiles', activeProfileId, 'learningPaths');
}
return collection(db, 'artifacts', appId, 'users', uid, 'learningPaths');
};
const getLearningPathDocRef = (uid, pathId) => {
if (isSubscriber && activeProfileId !== 'self') {
return doc(db, 'artifacts', appId, 'users', uid, 'profiles', activeProfileId, 'learningPaths', pathId);
}
return doc(db, 'artifacts', appId, 'users', uid, 'learningPaths', pathId);
};
const activeProfileName = (profiles.find(p => p.id === activeProfileId)?.name) || 'My Profile';
const switchProfile = (profileId) => {
// Free users only have self; guard anyway.
if (!isSubscriber && profileId !== 'self') {
setPostSubscribeState(gameState);
setShowSubscribeModal(true);
return;
}
setActiveProfileId(profileId);
setCurrentPath(null);
setCurrentLessonContent('');
setQuestions([]);
setUserAnswers({});
setScore(0);
setErrorMsg('');
// Keep user in same area; just refresh data.
};
const addSubProfile = async (name) => {
setProfileError('');
if (!user || user.isAnonymous) {
setShowAuthModal(true);
return;
}
if (!isSubscriber) {
setPostSubscribeState(gameState);
setShowSubscribeModal(true);
return;
}
const trimmed = (name || '').trim();
if (!trimmed) {
setProfileError('Please enter a profile name.');
return;
}
if (profiles.length >= 5) {
setProfileError('Profile limit reached (max 5 total profiles).');
return;
}
try {
const ref = doc(collection(db, 'artifacts', appId, 'users', user.uid, 'profiles'));
await setDoc(ref, {
name: trimmed,
createdAt: new Date().toISOString()
});
setActiveProfileId(ref.id);
} catch (e) {
console.error('Add profile error', e);
setProfileError(String(e?.message || e));
}
};
const deleteSubProfile = async (profileId) => {
setProfileError('');
if (!user || user.isAnonymous) return;
if (profileId === 'self') return;
if (!isSubscriber) return;
try {
// Best-effort delete learningPaths under this profile
const lpRef = collection(db, 'artifacts', appId, 'users', user.uid, 'profiles', profileId, 'learningPaths');
const snap = await getDocs(lpRef);
const deletes = snap.docs.map(d => deleteDoc(d.ref));
await Promise.all(deletes);
// Delete the profile document
const profRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profiles', profileId);
await deleteDoc(profRef);
if (activeProfileId === profileId) {
setActiveProfileId('self');
}
} catch (e) {
console.error('Delete profile error', e);
setProfileError(String(e?.message || e));
}
};
const handleLogout = async () => {
await signOut(auth);
setLearningPaths([]);
setNotes('');
await signInAnonymously(auth);
setGameState('menu');
};
const handleDeleteAccount = async () => {
if (!user || user.isAnonymous) return;
try {
const userDocRef = doc(db, 'artifacts', appId, 'users', user.uid, 'settings', 'profile');
const pathsRef = collection(db, 'artifacts', appId, 'users', user.uid, 'learningPaths');
const snapshot = await getDocs(pathsRef);
const deletePromises = snapshot.docs.map(d => deleteDoc(d.ref));
await Promise.all(deletePromises);
await deleteDoc(userDocRef);
await deleteUser(user);
setLearningPaths([]);
setNotes('');
await signInAnonymously(auth);
setGameState('menu');
setShowDeleteModal(false);
} catch (error) {
console.error("Error deleting account:", error);
throw error;
}
};
useEffect(() => {
let interval = null;
if (isTimerActive && timerSeconds > 0) {
interval = setInterval(() => {
setTimerSeconds(prev => prev - 1);
}, 1000);
} else if (timerSeconds === 0) {
setIsTimerActive(false);
}
return () => clearInterval(interval);
}, [isTimerActive, timerSeconds]);
const toggleTimer = () => setIsTimerActive(!isTimerActive);
const resetTimer = () => { setIsTimerActive(false); setTimerSeconds(25 * 60); };
const formatTime = (sec) => {
const m = Math.floor(sec / 60);
const s = sec % 60;
return `${m}:${s < 10 ? '0' : ''}${s}`;
};
const handleGetDailyFact = async () => {
if (dailyFact) return;
const prompt = `Generate a fascinating, short "Did you know?" educational fact about Science, History, or Technology. Keep it under 2 sentences. Respond in ${LANGUAGE_NAMES[language]}.`;
try {
const fact = await callGeminiAPI(prompt, 2, false);
setDailyFact(fact);
} catch (e) {
console.error("Fact error", e);
}
};
const handleAddToNotes = (title, content) => {
if (user && user.isAnonymous) {
alert("Guest User: You must register to save notes.");
setShowAuthModal(true);
return;
}
const timestamp = new Date().toLocaleString();
const newNoteEntry = `\n\n--- Saved from: ${title} (${timestamp}) ---\n${content}`;
setNotes(prev => prev + newNoteEntry);
setPreviousGameState(gameState);
setGameState('studyTools');
};
const openExportModal = (title, content) => {
setExportData({ title, content });
setShowExportModal(true);
};
const triggerExport = (format) => {
if (!exportData) return;
const { title, content } = exportData;
const filename = title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const year = new Date().getFullYear();
const footerText = `\n\n${'-'.repeat(20)}\n© ${year} All rights reserved to TheSo Smart Learning`;
const footerHtml = `
© ${year} All rights reserved to TheSo Smart Learning
`; if (format === 'pdf') { const printWindow = window.open('', '', 'height=600,width=800'); printWindow.document.write(`${content}
${footerHtml}`+footer; const source = 'data:application/vnd.ms-word;charset=utf-8,' + encodeURIComponent(sourceHTML); const fileDownload = document.createElement("a"); document.body.appendChild(fileDownload); fileDownload.href = source; fileDownload.download = `${filename}.doc`; fileDownload.click(); document.body.removeChild(fileDownload); } else if (format === 'ppt') { alert("PowerPoint export will download a slide outline text file."); const element = document.createElement("a"); const file = new Blob([`SLIDE 1: ${title}\n\nCONTENT:\n${content}${footerText}`], {type: 'text/plain'}); element.href = URL.createObjectURL(file); element.download = `${filename}_slides.txt`; document.body.appendChild(element); element.click(); } setShowExportModal(false); }; const handleSummarize = async () => { if (!summarizerInput.trim()) return; setGameState('loading'); const prompt = ` You are an expert study assistant. Summarize the following text for a student in ${LANGUAGE_NAMES[language]}. Text: "${summarizerInput}" Output format: 1. A brief overview paragraph (2-3 sentences). 2. Key Takeaways (bullet points). Use plain text formatting. No markdown. `; try { const result = await callGeminiAPI(prompt, 3, false); setSummarizerResult(result); setGameState('summarizer'); } catch (err) { setErrorMsg("Failed to summarize."); setGameState('error'); } }; const handleFileUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { setSummarizerInput(event.target.result); }; reader.readAsText(file); }; const handleGenerateMnemonic = async () => { if (!mnemonicInput.trim()) return; setGameState('loading'); const prompt = ` Create a mnemonic device to help memorize the following list of items or facts: "${mnemonicInput}" Respond in ${LANGUAGE_NAMES[language]}. Provide: 1. An Acronym (if applicable). 2. A Rhyme or Catchy Phrase. 3. A short visualization/story. Use plain text formatting. No markdown. `; try { const result = await callGeminiAPI(prompt, 3, false); setMnemonicResult(result); setGameState('mnemonic'); } catch (err) { setErrorMsg("Failed to create mnemonic."); setGameState('error'); } }; const handleImageUpload = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { setSelectedImage(reader.result); }; reader.readAsDataURL(file); } }; const handleSmartSolve = async () => { if (!selectedImage && !contentInput.trim()) return; setGameState('loading'); let base64Data = null; if (selectedImage) { base64Data = selectedImage.split(',')[1]; } const prompt = ` You are an expert tutor. Analyze the provided ${selectedImage ? 'image' : 'text'} problem. ${contentInput ? `Additional User Context/Question: "${contentInput}"` : ''} Task: 1. Identify the problem or question. 2. Solve it step-by-step. 3. Explain the underlying concept clearly. Respond in ${LANGUAGE_NAMES[language]}. Use plain text formatting. Do not use Markdown symbols (like # or **). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. `; try { const result = await callGeminiAPI(prompt, 3, false, base64Data); setExplanationResult(result); setGameState('smartSolver'); } catch (err) { setErrorMsg("Failed to analyze content."); setGameState('error'); } }; const handleGenerateQuizFromContent = async () => { if (!contentInput.trim() && !selectedImage) return; setGameState('loading'); let base64Data = null; if (selectedImage) { base64Data = selectedImage.split(',')[1]; } const prompt = ` Analyze the provided content (text or image) and generate a practice quiz in ${LANGUAGE_NAMES[language]}. Content/Context: "${contentInput}" Generate 5 multiple-choice questions based specifically on this content. Try to generate different questions if you have seen this content before. Return ONLY a valid RAW JSON Array of objects. Each object must have: "id" (number), "question" (string), "options" (array of 4 strings), "answer" (string), "explanation" (string). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. `; try { const qs = await callGeminiAPI(prompt, 3, true, base64Data); setQuestions(qs); setScore(0); setUserAnswers({}); setCurrentQuestionIndex(0); setTopic(contentInput.substring(0, 30) + "..."); setGameState('quiz'); } catch (err) { setErrorMsg("Failed to generate quiz from content."); setGameState('error'); } }; const handleGenerateQuizFromTopic = async () => { if (!customQuizTopic.trim()) return; setGameState('loading'); const prompt = ` Generate a practice quiz with ${customQuizCount} multiple-choice questions about "${customQuizTopic}". Respond in ${LANGUAGE_NAMES[language]}. Return ONLY a valid RAW JSON Array of objects. Each object must have: "id" (number), "question" (string), "options" (array of 4 strings), "answer" (string), "explanation" (string). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. `; try { const qs = await callGeminiAPI(prompt); setQuestions(qs); setScore(0); setUserAnswers({}); setCurrentQuestionIndex(0); setTopic(customQuizTopic); setGameState('quiz'); } catch (err) { setErrorMsg("Failed to generate custom quiz."); setGameState('error'); } }; const handleExportQuiz = () => { const formattedQuestions = questions.map((q, i) => { return `${i+1}. ${q.question}\n a) ${q.options[0]}\n b) ${q.options[1]}\n c) ${q.options[2]}\n d) ${q.options[3]}\n`; }).join('\n'); const formattedAnswers = questions.map((q, i) => `${i+1}. ${q.answer}`).join('\n'); const fullContent = `QUIZ: ${topic || 'Custom Quiz'}\n\n${formattedQuestions}\n\n${'-'.repeat(20)}\nANSWER KEY\n\n${formattedAnswers}`; openExportModal(`Quiz - ${topic || 'Custom'}`, fullContent); }; const handleQuickExplain = async () => { if (!topic.trim()) return; setGameState('loading'); const prompt = ` Provide a ${explanationLevel} explanation for the following topic or problem: "${topic}" Respond in ${LANGUAGE_NAMES[language]}. If it is a math problem, solve it step-by-step. If it is a concept, explain it clearly using analogies if appropriate for the level. Use plain text formatting. Do not use Markdown symbols (like # or **). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. `; try { const result = await callGeminiAPI(prompt, 3, false); setExplanationResult(result); setGameState('quickExplain'); } catch (err) { setErrorMsg("Failed to get explanation."); setGameState('error'); } }; const handleGenerateFlashcards = async () => { if (!topic.trim()) return; setGameState('loading'); const prompt = ` Generate 8-10 educational flashcards for the topic: "${topic}". Respond in ${LANGUAGE_NAMES[language]}. Return ONLY a valid RAW JSON Array of objects. Each object must have: "front" (string - term or question), "back" (string - definition or answer). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. `; try { const cards = await callGeminiAPI(prompt); if (!Array.isArray(cards)) throw new Error("Invalid format"); setFlashcards(cards); setCurrentCardIndex(0); setIsCardFlipped(false); setGameState('flashcards'); } catch (err) { setErrorMsg("Failed to generate flashcards."); setGameState('error'); } }; const handleExplainCard = async () => { const currentCard = flashcards[currentCardIndex]; if (!currentCard) return; setGameState('loading'); const prompt = ` Provide a clear, detailed explanation for the concept: "${currentCard.front}" in the context of the subject: "${topic}". Respond in ${LANGUAGE_NAMES[language]}. Use plain text formatting. Do not use Markdown symbols (like # or **). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. `; try { const result = await callGeminiAPI(prompt, 3, false); setExplanationResult(result); setGameState('flashcardExplain'); } catch (err) { setErrorMsg("Failed to get explanation."); setGameState('flashcards'); } }; const handleGradeEssay = async () => { if (!essayText.trim() || !topic.trim()) return; setGameState('loading'); const prompt = ` Act as a strict academic examiner. Grade the following essay on the topic: "${topic}". Respond in ${LANGUAGE_NAMES[language]}. Essay Content: "${essayText}" Return ONLY a valid RAW JSON object with these fields: - "score": number (0 to 100) - "feedback": string (A concise summary of strengths and weaknesses, max 3 sentences. Do not use LaTeX ($) or Markdown (*, #).) - "improvements": array of strings (3 specific bullet points on how to improve. Do not use LaTeX ($) or Markdown (*, #).) `; try { const result = await callGeminiAPI(prompt); setEssayResult(result); setGameState('essayGrader'); } catch (err) { setErrorMsg("Failed to grade essay."); setGameState('error'); } }; const handleCreatePath = async () => { if (!topic.trim()) return; // Free plan limit: 1 new subject per day (All-Access = unlimited) const todayKey = new Date().toISOString().slice(0, 10); if (!isSubscriber) { if (user && !user.isAnonymous) { const daily = userProfile?.dailyLearning || { date: "", count: 0 }; const countToday = daily.date === todayKey ? (daily.count || 0) : 0; if (countToday >= 1) { setErrorMsg("Daily limit reached: free accounts can start 1 new subject per day. Upgrade to All-Access for unlimited subjects."); setPostSubscribeState('pathSelect'); setShowSubscribeModal(true); return; } } else { // Anonymous users: enforce the same limit using localStorage try { const raw = localStorage.getItem('dailyLearningAnon'); const daily = raw ? JSON.parse(raw) : { date: "", count: 0 }; const countToday = daily?.date === todayKey ? (daily?.count || 0) : 0; if (countToday >= 1) { setErrorMsg("Daily limit reached: free access can start 1 new subject per day. Create an account or upgrade to All-Access for unlimited subjects."); setShowAuthModal(true); return; } } catch (_) {} } } setGameState('loading'); const prompt = ` Create a comprehensive learning path syllabus for the subject: "${topic}". Break it down into 5-8 sequential modules/topics that go from beginner to advanced. Respond in ${LANGUAGE_NAMES[language]}. Return ONLY a valid RAW JSON Array of objects. Each object must have: "title" (string), "description" (string). Do not nest the array in an object. `; try { const modulesData = await callGeminiAPI(prompt); if (!Array.isArray(modulesData)) throw new Error("Invalid format"); const newPath = { subject: topic, type: 'general', createdAt: new Date().toISOString(), modules: modulesData.map((m) => ({ ...m, status: 'unlocked', score: null })) }; if (user && !user.isAnonymous) { await setDoc(doc(getLearningPathsCollectionRef(user.uid)), newPath); } else { const localPath = { ...newPath, id: Date.now().toString() }; setLearningPaths(prev => [...prev, localPath]); setCurrentPath(localPath); } // Update daily subject counter for free users if (!isSubscriber) { const todayKey2 = new Date().toISOString().slice(0, 10); if (user && !user.isAnonymous) { const profileRef2 = doc(db, 'artifacts', appId, 'users', user.uid, 'settings', 'profile'); const daily2 = userProfile?.dailyLearning || { date: "", count: 0 }; const countToday2 = daily2.date === todayKey2 ? (daily2.count || 0) : 0; const nextDaily2 = { date: todayKey2, count: countToday2 + 1 }; try { await updateDoc(profileRef2, { dailyLearning: nextDaily2 }); } catch (_) {} setUserProfile(prev => ({ ...(prev || {}), dailyLearning: nextDaily2 })); } else { try { const raw2 = localStorage.getItem('dailyLearningAnon'); const dailyAnon2 = raw2 ? JSON.parse(raw2) : { date: "", count: 0 }; const countTodayAnon2 = dailyAnon2?.date === todayKey2 ? (dailyAnon2?.count || 0) : 0; localStorage.setItem('dailyLearningAnon', JSON.stringify({ date: todayKey2, count: countTodayAnon2 + 1 })); } catch (_) {} } } setTopic(''); setGameState('pathSelect'); } catch (err) { setErrorMsg("Failed to generate path."); setGameState('error'); } }; const handleSelectPath = (path) => { setCurrentPath(path); setGameState('pathDashboard'); }; const handleViewLesson = async (index) => { if (!currentPath || !currentPath.modules) return; setCurrentModuleIndex(index); setGameState('loading'); const module = currentPath.modules[index]; const isExam = currentPath.type === 'exam'; let prompt; if (isExam) { prompt = ` You are an expert tutor preparing a student for the "${currentPath.examName}" exam. Write a detailed study lesson for the topic "${module.title}" in the subject "${currentPath.subject}". Respond in ${LANGUAGE_NAMES[language]}. Structure the lesson as follows: 1. Concept Explanation: Clear, concise explanation of the topic. 2. Key Formulas/Rules: (If applicable) Important rules to memorize. 3. Worked Examples: Provide 2 step-by-step examples similar to what appears on the ${currentPath.examName}. 4. Exam Tips: Common pitfalls and strategies for this topic. Use plain text formatting. Do not use Markdown symbols (like # or **). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. Use natural spacing to separate sections. `; } else { prompt = ` Write a comprehensive educational lesson about "${module.title}" specifically for the subject "${currentPath.subject}". The lesson should be detailed, clear, and explain concepts topic by topic. Respond in ${LANGUAGE_NAMES[language]}. Use plain text formatting. Do not use Markdown symbols (like # or **). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. Use natural spacing and numbering to structure the text. Return plain text. `; } try { const content = await callGeminiAPI(prompt, 3, false); setCurrentLessonContent(content); setGameState('lessonView'); } catch (err) { setErrorMsg("Failed to load lesson."); setGameState('error'); } }; const handleAddTopicToPath = async () => { if (!newTopicInput.trim() || !currentPath) return; const newModule = { title: newTopicInput, description: "User added topic", status: 'unlocked', score: null }; if (user && !user.isAnonymous) { const pathRef = getLearningPathDocRef(user.uid, currentPath.id); const updatedModules = [...currentPath.modules, newModule]; await updateDoc(pathRef, { modules: updatedModules }); } else { const updatedPath = { ...currentPath, modules: [...currentPath.modules, newModule] }; setCurrentPath(updatedPath); setLearningPaths(prev => prev.map(p => p.id === updatedPath.id ? updatedPath : p)); } setNewTopicInput(''); }; const handleStartPathQuiz = async (isRetake = false) => { if (!currentPath || !currentPath.modules) return; setGameState('loading'); const module = currentPath.modules[currentModuleIndex]; const isExam = currentPath.type === 'exam'; const variationInstruction = isRetake ? "Generate a completely NEW set of questions different from a standard set. " : ""; let prompt; if (isExam) { prompt = ` ${variationInstruction} Generate a practice quiz with 10 multiple-choice questions about "${module.title}" for the "${currentPath.examName}" exam in "${currentPath.subject}". The difficulty and style must match the ${currentPath.examName}. Respond in ${LANGUAGE_NAMES[language]}. Return ONLY a valid RAW JSON Array of objects. Each object must have: "id" (number), "question" (string), "options" (array of 4 strings), "answer" (string), "explanation" (string). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. Do not use Markdown symbols like '**' or '##'. `; } else { prompt = ` ${variationInstruction} Generate a quiz with 10 multiple-choice questions about "${module.title}" in the context of "${currentPath.subject}". Respond in ${LANGUAGE_NAMES[language]}. Return ONLY a valid RAW JSON Array of objects. Each object must have: "id" (number), "question" (string), "options" (array of 4 strings), "answer" (string), "explanation" (string). Do not use LaTeX math delimiters like '$'. Use plain text or Unicode for math. Do not use Markdown symbols like '**' or '##'. `; } try { const qs = await callGeminiAPI(prompt); setQuestions(qs); setScore(0); setUserAnswers({}); setCurrentQuestionIndex(0); setGameState('quiz'); } catch (err) { setErrorMsg("Failed to generate quiz."); setGameState('error'); } }; const handleGenerateExamPath = async (subject) => { setGameState('loading'); const examName = EXAM_DATA[selectedExamId].name; const prompt = ` Create a comprehensive Exam Preparation Syllabus for "${subject}" specifically for the "${examName}" exam. Break it down into 6-8 sequential topics (modules) that a student must master. Respond in ${LANGUAGE_NAMES[language]}. Return ONLY a valid RAW JSON Array of objects. Each object must have: "title" (string), "description" (string - brief summary of what this topic covers). Do not nest the array in an object. `; try { const modulesData = await callGeminiAPI(prompt); if (!Array.isArray(modulesData)) throw new Error("Invalid format"); const newPath = { subject: `${examName}: ${subject}`, examName: examName, type: 'exam', createdAt: new Date().toISOString(), modules: modulesData.map((m) => ({ ...m, status: 'unlocked', score: null })) }; if (user && !user.isAnonymous) { const pathRef = doc(getLearningPathsCollectionRef(user.uid)); setCurrentPath({ ...newPath, id: pathRef.id }); await setDoc(pathRef, newPath); } else { const localPath = { ...newPath, id: Date.now().toString() }; setLearningPaths(prev => [...prev, localPath]); setCurrentPath(localPath); } setGameState('pathDashboard'); } catch (err) { setErrorMsg("Failed to generate exam plan. Please try again."); setGameState('error'); } }; const handleAnswer = (option) => { const currentQ = questions[currentQuestionIndex]; if (userAnswers[currentQ.id]) return; const isCorrect = option === currentQ.answer; setUserAnswers(prev => ({ ...prev, [currentQ.id]: { selected: option, isCorrect } })); if (isCorrect) setScore(prev => prev + 1); }; const nextQuestion = () => { if (currentQuestionIndex < questions.length - 1) { setCurrentQuestionIndex(prev => prev + 1); } else { if (currentPath) { finishPathModule(); } else { setGameState('results'); } } }; const finishPathModule = async () => { const percentage = (score / questions.length) * 100; const passed = percentage >= 80; if (passed) { const updatedModules = [...currentPath.modules]; updatedModules[currentModuleIndex].status = 'completed'; updatedModules[currentModuleIndex].score = percentage; if (user && !user.isAnonymous) { const pathRef = getLearningPathDocRef(user.uid, currentPath.id); await updateDoc(pathRef, { modules: updatedModules }); } else { const updatedPath = { ...currentPath, modules: updatedModules }; setCurrentPath(updatedPath); setLearningPaths(prev => prev.map(p => p.id === updatedPath.id ? updatedPath : p)); } } setGameState('results'); }; const handleAskAI = (questionText) => { setIsChatOpen(true); const contextMsg = `I need help understanding this question: "${questionText}". Can you explain how to solve it? Respond in ${LANGUAGE_NAMES[language]}.`; handleChatSubmit(contextMsg, true); }; const handleChatSubmit = async (text, isAuto = false) => { if (!text.trim()) return; if (!isAuto) { setChatMessages(prev => [...prev, { role: 'user', text }]); setChatInput(''); } else { setChatMessages(prev => [...prev, { role: 'user', text }]); } setIsChatLoading(true); const systemPrompt = ` You are an expert AI Tutor. Respond in ${LANGUAGE_NAMES[language]}. Provide clear, helpful explanations broken down into short, readable paragraphs. Do NOT use Markdown headers (like ###). Do NOT use bolding (**). Do NOT use LaTeX math delimiters (like $). Use standard plain text and Unicode symbols for math. `; try { const response = await callGeminiAPI(systemPrompt + "\n\nUser: " + text, 3, false); setChatMessages(prev => [...prev, { role: 'assistant', text: response }]); if (isReading) { const u = new SpeechSynthesisUtterance(response); window.speechSynthesis.speak(u); } } catch (e) { setChatMessages(prev => [...prev, { role: 'assistant', text: "Error connecting to AI." }]); } finally { setIsChatLoading(false); } }; // --- Components --- const AuthModal = () => { const [mode, setMode] = useState('login'); const [formData, setFormData] = useState({ name: '', email: '', phone: '', password: '' }); const [gdprChecked, setGdprChecked] = useState(false); const [subscribeAllAccess, setSubscribeAllAccess] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); if (!showAuthModal) return null; const handleSubmit = async () => { setError(''); if (mode === 'register') { if (!gdprChecked) { setError("You must agree to the privacy policy to register."); return; } if (!formData.name || !formData.email || !formData.password || !formData.phone) { setError("All fields are required."); return; } } else { if (!formData.email || !formData.password) { setError("Email and password are required."); return; } } setLoading(true); try { if (mode === 'register') { const userCredential = await createUserWithEmailAndPassword(auth, formData.email, formData.password); await updateProfile(userCredential.user, { displayName: formData.name }); await setDoc(doc(db, 'artifacts', appId, 'users', userCredential.user.uid, 'settings', 'profile'), { name: formData.name, email: formData.email, phone: formData.phone, gdprConsent: true, gdprDate: new Date().toISOString(), role: 'student', dailyLearning: { date: '', count: 0 }, subscription: subscribeAllAccess ? { plan: 'all_access', status: 'active', startDate: new Date().toISOString() } : { plan: 'free', status: 'inactive' }, }); } else { await signInWithEmailAndPassword(auth, formData.email, formData.password); } setShowAuthModal(false); setGameState('menu'); } catch (e) { console.error(e); setError(String(e?.message || e)); } finally { setLoading(false); } }; return ({mode === 'login' ? 'Sign in to access your progress.' : 'Join to save your learning journey.'}
{error &&Note: This is a subscription flag in the app. To charge payments, connect Stripe/PayPal later.
{canUseProfiles ? 'Premium: create up to 5 profiles (family).' : 'Free: only your main profile.'}
Multiple profiles are available only with the All-Access subscription.
Tip: each profile has its own courses, grades, and certificates.
This action is permanent and cannot be undone. All your learning progress, notes, and profile data will be permanently deleted in compliance with GDPR.
{error &&Unlock every feature on TheSo Smart Learning.
This activation is a placeholder flag for your MVP. Connect Stripe/PayPal later to charge real subscriptions.
This admin panel uses your backend (Cloud Functions or Express) to create Stripe promo codes and grant temporary access. Configure BACKEND_BASE_URL and (for checkout) STRIPE_ALL_ACCESS_PRICE_ID at the top of this file.
{adminMsg && (Backend endpoints expected
{t('chatWelcome')}
{errorMsg || "We couldn't generate the content. Please try again."}
{isGuest ? "Session-only Mode" : "Student Dashboard"}
{isGuest && ({learningPaths.length}
{completedModules} / {totalModules}
{avgScore}%
You haven't completed any topics in this course yet.
You haven't started any learning paths yet.
{t('quizGenDesc')}
{t('quizTopicDesc')}
{t('photoSolverDesc')}
{t('uploadImage')}
{flashcards[currentCardIndex].front}
{t('flipCard')}
{flashcards[currentCardIndex].back}
Context: {topic}
{essayResult.feedback}
{t('uploadImage')}
This official document certifies that
has successfully completed the comprehensive curriculum for
Final Grade
{averageGrade}%
Study Hours
{studyHours}+
Date Issued
{completedDate}
Academic Director
ID: {certificateId}
Lead Instructor
Loading lesson data...
{currentPath.subject}
Loading path...
{currentPath.modules.length} {isExam ? 'Topics' : 'Modules'} • {isExam ? 'Exam Study Plan' : 'Learning Path'}
{m.status === 'completed' ? `Score: ${m.score}%` : m.description}
No paths created yet.
}Master your exam with AI-generated study plans.
No study plans created yet. Select a subject above.
}Loading quiz...
{t('score')}: {score}/{questions.length} ({percentage}%)
{isPathMode && ({t('passRequirement')}
{t('tagline')}. Get instant explanations, personalized study plans, and exam preparation.
{dailyFact || "Click the button to learn something new today!"}