Spaces:
Running
Running
| /*Huggingface 1/11/26*/ | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Spelling Trainer | AI-Powered Learning</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;400;600;700&family=Exo+2:wght@300;400;600;800&display=swap" rel="stylesheet"> | |
| <style> | |
| /* === FUTURISTIC THEME === */ | |
| :root { | |
| --primary: #00f3ff; | |
| --secondary: #9d00ff; | |
| --accent: #ff00ff; | |
| --success: #00ff9d; | |
| --warning: #ffd600; | |
| --danger: #ff006e; | |
| --bg: #0a0a1a; | |
| --card: #101025; | |
| --surface: #1a1a3a; | |
| --text: #ffffff; | |
| --muted: #8a8aff; | |
| --glow: 0 0 40px rgba(0, 243, 255, 0.5); | |
| --pulse: 0 0 80px rgba(157, 0, 255, 0.3); | |
| --transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| html { | |
| scroll-behavior: smooth; | |
| overflow-x: hidden; | |
| } | |
| body { | |
| font-family: 'Exo 2', sans-serif; | |
| background-color: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| position: relative; | |
| letter-spacing: 0.5px; | |
| } | |
| /* === ADVANCED BACKGROUND === */ | |
| .neural-network { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -3; | |
| background: | |
| radial-gradient(circle at 20% 30%, rgba(0, 243, 255, 0.08) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 70%, rgba(157, 0, 255, 0.06) 0%, transparent 50%), | |
| radial-gradient(circle at 40% 50%, rgba(255, 0, 255, 0.04) 0%, transparent 50%), | |
| linear-gradient(135deg, #0a0a1a 0%, #0d0d23 25%, #101025 50%, #0d0d23 75%, #0a0a1a 100%); | |
| background-size: 400% 400%; | |
| animation: neuralFlow 20s ease infinite; | |
| } | |
| @keyframes neuralFlow { | |
| 0%, 100% { background-position: 0% 0%; } | |
| 33% { background-position: 100% 50%; } | |
| 66% { background-position: 50% 100%; } | |
| } | |
| .synapse-layer { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 200%; | |
| height: 200%; | |
| z-index: -2; | |
| background-image: | |
| repeating-linear-gradient(90deg, transparent, transparent 98px, rgba(0, 243, 255, 0.02) 98px, rgba(0, 243, 255, 0.02) 100px), | |
| repeating-linear-gradient(0deg, transparent, transparent 98px, rgba(157, 0, 255, 0.02) 98px, rgba(157, 0, 255, 0.02) 100px); | |
| animation: synapseMove 40s linear infinite; | |
| transform-origin: center; | |
| } | |
| @keyframes synapseMove { | |
| 0% { transform: translate(0, 0) rotate(0deg); } | |
| 100% { transform: translate(-50%, -50%) rotate(360deg); } | |
| } | |
| .neuron-grid { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| background-image: radial-gradient(circle at var(--x, 50%) var(--y, 50%), rgba(255, 0, 255, 0.1) 0%, transparent 10%); | |
| animation: neuronPulse 3s ease-in-out infinite; | |
| } | |
| @keyframes neuronPulse { | |
| 0%, 100% { opacity: 0.3; } | |
| 50% { opacity: 0.8; } | |
| } | |
| .data-stream { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| background: linear-gradient(90deg, transparent 30%, rgba(0, 243, 255, 0.05) 50%, transparent 70%); | |
| animation: dataStream 15s linear infinite; | |
| filter: blur(1px); | |
| } | |
| @keyframes dataStream { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(100%); } | |
| } | |
| /* === CELEBRATION EFFECTS === */ | |
| .celebration-container { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 1000; | |
| overflow: hidden; | |
| } | |
| .confetti { | |
| position: absolute; | |
| width: 12px; | |
| height: 12px; | |
| opacity: 0; | |
| animation: confettiFall 3s ease-out forwards; | |
| } | |
| @keyframes confettiFall { | |
| 0% { | |
| transform: translateY(-100px) rotate(0deg); | |
| opacity: 1; | |
| } | |
| 100% { | |
| transform: translateY(100vh) rotate(360deg); | |
| opacity: 0; | |
| } | |
| } | |
| /* === MAIN CONTAINER === */ | |
| .app-container { | |
| max-width: 1800px; | |
| margin: 0 auto; | |
| padding: 30px; | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 30px; | |
| min-height: 100vh; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| /* === HEADER === */ | |
| .neural-header { | |
| text-align: center; | |
| padding: 40px 30px; | |
| margin-bottom: 30px; | |
| background: rgba(16, 16, 37, 0.7); | |
| backdrop-filter: blur(20px); | |
| border-radius: 30px; | |
| border: 1px solid rgba(0, 243, 255, 0.2); | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: var(--glow); | |
| } | |
| .neural-header::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(45deg, transparent, rgba(0, 243, 255, 0.1), transparent); | |
| transform: translateX(-100%); | |
| animation: headerShine 8s infinite; | |
| } | |
| @keyframes headerShine { | |
| 0% { transform: translateX(-100%); } | |
| 50%, 100% { transform: translateX(100%); } | |
| } | |
| .header-title { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 4.5rem; | |
| font-weight: 900; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary), var(--accent)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 20px; | |
| letter-spacing: 4px; | |
| text-shadow: 0 0 50px rgba(0, 243, 255, 0.5); | |
| animation: titleGlitch 10s infinite; | |
| } | |
| @keyframes titleGlitch { | |
| 0%, 100% { transform: translate(0); } | |
| 98% { transform: translate(0); } | |
| 99% { transform: translate(-2px, 1px); } | |
| } | |
| .header-subtitle { | |
| font-family: 'Rajdhani', sans-serif; | |
| font-size: 1.4rem; | |
| color: var(--muted); | |
| max-width: 800px; | |
| margin: 0 auto; | |
| font-weight: 300; | |
| letter-spacing: 2px; | |
| } | |
| /* === MAIN GRID === */ | |
| .neural-grid { | |
| display: grid; | |
| grid-template-columns: 350px 1fr; | |
| gap: 30px; | |
| min-height: 80vh; | |
| } | |
| @media (max-width: 1400px) { | |
| .neural-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* === SIDEBAR === */ | |
| .neural-sidebar { | |
| background: rgba(16, 16, 37, 0.8); | |
| backdrop-filter: blur(30px); | |
| border-radius: 25px; | |
| padding: 30px; | |
| border: 1px solid rgba(0, 243, 255, 0.2); | |
| box-shadow: var(--glow); | |
| height: fit-content; | |
| position: sticky; | |
| top: 30px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 35px; | |
| } | |
| @media (max-width: 1400px) { | |
| .neural-sidebar { | |
| position: static; | |
| } | |
| } | |
| .sidebar-section { | |
| padding-bottom: 30px; | |
| border-bottom: 1px solid rgba(0, 243, 255, 0.1); | |
| } | |
| .sidebar-section:last-child { | |
| border-bottom: none; | |
| padding-bottom: 0; | |
| } | |
| .section-title { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.1rem; | |
| color: var(--primary); | |
| margin-bottom: 25px; | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .section-title i { | |
| font-size: 1.3rem; | |
| color: var(--accent); | |
| filter: drop-shadow(0 0 8px currentColor); | |
| } | |
| /* === FORM ELEMENTS === */ | |
| .input-group { | |
| margin-bottom: 25px; | |
| } | |
| .label { | |
| display: block; | |
| margin-bottom: 12px; | |
| color: var(--text); | |
| font-family: 'Rajdhani', sans-serif; | |
| font-weight: 600; | |
| font-size: 0.95rem; | |
| letter-spacing: 1px; | |
| } | |
| .select, .input { | |
| width: 100%; | |
| padding: 16px 20px; | |
| background: rgba(26, 26, 58, 0.8); | |
| border: 1px solid rgba(0, 243, 255, 0.3); | |
| border-radius: 15px; | |
| color: var(--text); | |
| font-family: 'Exo 2', sans-serif; | |
| font-size: 1rem; | |
| transition: var(--transition); | |
| outline: none; | |
| } | |
| .select:focus, .input:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 4px rgba(0, 243, 255, 0.2); | |
| } | |
| .select option { | |
| background: var(--card); | |
| color: var(--text); | |
| padding: 10px; | |
| } | |
| /* === BUTTONS === */ | |
| .btn { | |
| padding: 18px 25px; | |
| border-radius: 15px; | |
| border: none; | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 12px; | |
| position: relative; | |
| overflow: hidden; | |
| letter-spacing: 1px; | |
| } | |
| .btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); | |
| transition: 0.6s; | |
| } | |
| .btn:hover::before { | |
| left: 100%; | |
| } | |
| .btn.primary { | |
| background: linear-gradient(135deg, var(--primary), #0088cc); | |
| color: #000; | |
| border: 2px solid rgba(0, 243, 255, 0.5); | |
| box-shadow: 0 10px 40px rgba(0, 243, 255, 0.3); | |
| } | |
| .btn.primary:hover { | |
| transform: translateY(-4px) scale(1.02); | |
| box-shadow: 0 20px 60px rgba(0, 243, 255, 0.5); | |
| } | |
| .btn.secondary { | |
| background: linear-gradient(135deg, var(--secondary), #7d00cc); | |
| color: white; | |
| border: 2px solid rgba(157, 0, 255, 0.5); | |
| box-shadow: 0 10px 40px rgba(157, 0, 255, 0.3); | |
| } | |
| .btn.secondary:hover { | |
| transform: translateY(-4px) scale(1.02); | |
| box-shadow: 0 20px 60px rgba(157, 0, 255, 0.5); | |
| } | |
| .btn.success { | |
| background: linear-gradient(135deg, var(--success), #00cc7a); | |
| color: #000; | |
| border: 2px solid rgba(0, 255, 157, 0.5); | |
| box-shadow: 0 10px 40px rgba(0, 255, 157, 0.3); | |
| } | |
| .btn.success:hover { | |
| transform: translateY(-4px) scale(1.02); | |
| box-shadow: 0 20px 60px rgba(0, 255, 157, 0.5); | |
| } | |
| .btn.warning { | |
| background: linear-gradient(135deg, var(--warning), #ccaa00); | |
| color: #000; | |
| border: 2px solid rgba(255, 214, 0, 0.5); | |
| box-shadow: 0 10px 40px rgba(255, 214, 0, 0.3); | |
| } | |
| .btn.warning:hover { | |
| transform: translateY(-4px) scale(1.02); | |
| box-shadow: 0 20px 60px rgba(255, 214, 0, 0.5); | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none ; | |
| box-shadow: none ; | |
| } | |
| /* === GOOGLE TTS CONFIG === */ | |
| .tts-config { | |
| background: rgba(0, 243, 255, 0.05); | |
| border-radius: 20px; | |
| padding: 25px; | |
| border: 1px solid rgba(0, 243, 255, 0.3); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .tts-config::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| } | |
| /* === MAIN CONTENT === */ | |
| .neural-content { | |
| background: rgba(16, 16, 37, 0.8); | |
| backdrop-filter: blur(30px); | |
| border-radius: 30px; | |
| border: 1px solid rgba(0, 243, 255, 0.2); | |
| box-shadow: var(--glow); | |
| overflow: hidden; | |
| min-height: 600px; | |
| } | |
| /* === TAB SYSTEM === */ | |
| .tabs { | |
| display: flex; | |
| background: rgba(10, 10, 26, 0.9); | |
| border-bottom: 1px solid rgba(0, 243, 255, 0.2); | |
| position: relative; | |
| overflow-x: auto; | |
| } | |
| .tab { | |
| flex: 1; | |
| min-width: 200px; | |
| padding: 25px 30px; | |
| background: transparent; | |
| border: none; | |
| color: var(--muted); | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| position: relative; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 15px; | |
| letter-spacing: 1.5px; | |
| } | |
| .tab::before { | |
| content: ''; | |
| position: absolute; | |
| bottom: 0; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, var(--primary), var(--accent)); | |
| transition: var(--transition); | |
| } | |
| .tab:hover { | |
| color: var(--primary); | |
| background: rgba(0, 243, 255, 0.05); | |
| } | |
| .tab.active { | |
| color: var(--primary); | |
| background: rgba(0, 243, 255, 0.1); | |
| } | |
| .tab.active::before { | |
| width: 80%; | |
| } | |
| .panel { | |
| padding: 40px; | |
| display: none; | |
| animation: panelFade 0.6s ease; | |
| } | |
| @keyframes panelFade { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .panel.active { | |
| display: block; | |
| } | |
| /* === GAME HEADER === */ | |
| .game-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 40px; | |
| padding-bottom: 30px; | |
| border-bottom: 1px solid rgba(0, 243, 255, 0.2); | |
| flex-wrap: wrap; | |
| gap: 30px; | |
| } | |
| .progress-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 25px; | |
| flex: 1; | |
| min-width: 400px; | |
| } | |
| .progress { | |
| flex: 1; | |
| height: 16px; | |
| background: rgba(0, 243, 255, 0.1); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| border-radius: 8px; | |
| transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .progress-fill::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); | |
| animation: progressShimmer 2s infinite; | |
| } | |
| @keyframes progressShimmer { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(100%); } | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | |
| gap: 20px; | |
| flex: 2; | |
| min-width: 500px; | |
| } | |
| @media (max-width: 1200px) { | |
| .stats { | |
| min-width: 100%; | |
| } | |
| } | |
| .stat-card { | |
| background: rgba(26, 26, 58, 0.6); | |
| border-radius: 20px; | |
| padding: 25px; | |
| border: 1px solid rgba(0, 243, 255, 0.2); | |
| transition: var(--transition); | |
| position: relative; | |
| overflow: hidden; | |
| text-align: center; | |
| } | |
| .stat-card:hover { | |
| transform: translateY(-5px); | |
| border-color: var(--primary); | |
| box-shadow: var(--glow); | |
| } | |
| .stat-value { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 2.8rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| margin-bottom: 8px; | |
| text-shadow: 0 0 20px rgba(0, 243, 255, 0.5); | |
| } | |
| .stat-label { | |
| font-family: 'Rajdhani', sans-serif; | |
| font-size: 1rem; | |
| color: var(--muted); | |
| font-weight: 600; | |
| letter-spacing: 1.5px; | |
| text-transform: uppercase; | |
| } | |
| /* === WORD DISPLAY === */ | |
| .word-display-container { | |
| background: rgba(26, 26, 58, 0.4); | |
| border-radius: 25px; | |
| padding: 60px 40px; | |
| margin: 50px 0; | |
| border: 2px solid rgba(0, 243, 255, 0.2); | |
| transition: var(--transition); | |
| position: relative; | |
| overflow: hidden; | |
| min-height: 300px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .word-display-container::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| border-radius: 25px; | |
| padding: 3px; | |
| background: linear-gradient(135deg, var(--primary), transparent, var(--accent)); | |
| -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); | |
| mask-composite: exclude; | |
| pointer-events: none; | |
| opacity: 0.5; | |
| } | |
| .word-display { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 4.5rem; | |
| font-weight: 900; | |
| letter-spacing: 25px; | |
| color: var(--text); | |
| text-align: center; | |
| text-shadow: 0 5px 20px rgba(0, 0, 0, 0.5); | |
| line-height: 1.3; | |
| word-break: break-word; | |
| padding: 0 30px; | |
| filter: drop-shadow(0 0 10px rgba(0, 243, 255, 0.3)); | |
| } | |
| @media (max-width: 900px) { | |
| .word-display { | |
| font-size: 3rem; | |
| letter-spacing: 15px; | |
| padding: 0 20px; | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| .word-display { | |
| font-size: 2.2rem; | |
| letter-spacing: 10px; | |
| padding: 0 15px; | |
| } | |
| } | |
| .word-context { | |
| font-family: 'Exo 2', sans-serif; | |
| font-size: 0rem; | |
| color: var(--muted); | |
| font-style: italic; | |
| text-align: center; | |
| max-width: 900px; | |
| margin: 30px auto 0; | |
| padding: 0 30px; | |
| font-weight: 300; | |
| line-height: 1.6; | |
| } | |
| /* === AUDIO CONTROLS === */ | |
| .audio-controls-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); | |
| gap: 20px; | |
| margin: 50px 0; | |
| } | |
| /* === INPUT AREA === */ | |
| .input-container { | |
| margin: 60px 0; | |
| position: relative; | |
| } | |
| .input-label { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.2rem; | |
| color: var(--primary); | |
| margin-bottom: 25px; | |
| display: block; | |
| text-align: center; | |
| letter-spacing: 2px; | |
| } | |
| .answer-input { | |
| width: 100%; | |
| padding: 30px 40px; | |
| font-size: 2rem; | |
| font-family: 'Rajdhani', sans-serif; | |
| background: rgba(26, 26, 58, 0.6); | |
| border: 2px solid rgba(0, 243, 255, 0.3); | |
| border-radius: 20px; | |
| color: var(--text); | |
| text-align: center; | |
| transition: var(--transition); | |
| outline: none; | |
| letter-spacing: 3px; | |
| } | |
| @media (max-width: 900px) { | |
| .answer-input { | |
| font-size: 1.6rem; | |
| padding: 25px 30px; | |
| } | |
| } | |
| .answer-input:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 5px rgba(0, 243, 255, 0.2); | |
| } | |
| .answer-input::placeholder { | |
| color: rgba(138, 138, 255, 0.5); | |
| font-size: 1.6rem; | |
| letter-spacing: 2px; | |
| } | |
| .hint-text { | |
| text-align: center; | |
| color: var(--muted); | |
| margin-top: 20px; | |
| font-size: 1rem; | |
| opacity: 0.8; | |
| font-family: 'Rajdhani', sans-serif; | |
| } | |
| /* === ACTION BUTTONS === */ | |
| .action-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 25px; | |
| margin-top: 60px; | |
| } | |
| /* === FEEDBACK === */ | |
| .feedback-container { | |
| margin-top: 40px; | |
| padding: 30px; | |
| border-radius: 20px; | |
| text-align: center; | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| display: none; | |
| animation: feedbackPop 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| position: relative; | |
| overflow: hidden; | |
| font-family: 'Orbitron', sans-serif; | |
| } | |
| @keyframes feedbackPop { | |
| 0% { transform: scale(0.5); opacity: 0; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| .feedback-container.correct { | |
| background: rgba(0, 255, 157, 0.15); | |
| border: 2px solid rgba(0, 255, 157, 0.4); | |
| color: var(--success); | |
| display: block; | |
| } | |
| .feedback-container.incorrect { | |
| background: rgba(255, 214, 0, 0.15); | |
| border: 2px solid rgba(255, 214, 0, 0.4); | |
| color: var(--warning); | |
| display: block; | |
| } | |
| /* === TOAST === */ | |
| .toast-container { | |
| position: fixed; | |
| bottom: 40px; | |
| right: 40px; | |
| z-index: 10000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| max-width: 450px; | |
| } | |
| @media (max-width: 900px) { | |
| .toast-container { | |
| bottom: 20px; | |
| right: 20px; | |
| left: 20px; | |
| max-width: none; | |
| } | |
| } | |
| .toast { | |
| background: rgba(16, 16, 37, 0.95); | |
| border-radius: 20px; | |
| padding: 25px 30px; | |
| border-left: 5px solid var(--primary); | |
| box-shadow: var(--glow); | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| transform: translateX(100%); | |
| opacity: 0; | |
| animation: toastSlide 0.4s forwards; | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(0, 243, 255, 0.2); | |
| } | |
| @keyframes toastSlide { | |
| to { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| } | |
| .toast-icon { | |
| font-size: 1.8rem; | |
| filter: drop-shadow(0 0 8px currentColor); | |
| } | |
| .toast-content { | |
| flex: 1; | |
| } | |
| .toast-title { | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| color: var(--text); | |
| } | |
| .toast-message { | |
| font-family: 'Exo 2', sans-serif; | |
| font-size: 0.95rem; | |
| color: var(--muted); | |
| line-height: 1.5; | |
| } | |
| .toast-close { | |
| background: transparent; | |
| border: none; | |
| color: var(--muted); | |
| cursor: pointer; | |
| font-size: 1.5rem; | |
| transition: var(--transition); | |
| } | |
| .toast-close:hover { | |
| color: var(--danger); | |
| } | |
| /* === MODAL === */ | |
| .modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(10, 10, 26, 0.95); | |
| backdrop-filter: blur(30px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 20000; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: var(--transition); | |
| } | |
| .modal.active { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .modal-content { | |
| background: rgba(16, 16, 37, 0.95); | |
| border-radius: 30px; | |
| padding: 50px; | |
| max-width: 700px; | |
| width: 90%; | |
| border: 1px solid rgba(0, 243, 255, 0.3); | |
| box-shadow: var(--glow); | |
| transform: translateY(50px) scale(0.95); | |
| transition: var(--transition); | |
| position: relative; | |
| } | |
| .modal.active .modal-content { | |
| transform: translateY(0) scale(1); | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 40px; | |
| } | |
| .modal-title { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 2.5rem; | |
| color: var(--primary); | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .modal-close { | |
| background: transparent; | |
| border: none; | |
| color: var(--muted); | |
| font-size: 2rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| width: 50px; | |
| height: 50px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 50%; | |
| } | |
| .modal-close:hover { | |
| color: var(--danger); | |
| background: rgba(255, 0, 110, 0.1); | |
| } | |
| /* === DASHBOARD === */ | |
| .dashboard-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
| gap: 30px; | |
| } | |
| .dashboard-card { | |
| background: rgba(26, 26, 58, 0.6); | |
| border-radius: 25px; | |
| padding: 30px; | |
| border: 1px solid rgba(0, 243, 255, 0.2); | |
| transition: var(--transition); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .dashboard-card:hover { | |
| transform: translateY(-8px); | |
| border-color: var(--primary); | |
| box-shadow: var(--glow); | |
| } | |
| .dashboard-card h3 { | |
| font-family: 'Orbitron', sans-serif; | |
| color: var(--primary); | |
| margin-bottom: 25px; | |
| font-size: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .dashboard-card h3 i { | |
| font-size: 1.6rem; | |
| color: var(--accent); | |
| filter: drop-shadow(0 0 10px currentColor); | |
| } | |
| /* === BADGES === */ | |
| .badge-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); | |
| gap: 20px; | |
| margin-top: 20px; | |
| } | |
| .badge { | |
| background: rgba(16, 16, 37, 0.8); | |
| border-radius: 20px; | |
| padding: 25px; | |
| text-align: center; | |
| border: 2px solid transparent; | |
| transition: var(--transition); | |
| position: relative; | |
| overflow: hidden; | |
| cursor: pointer; | |
| } | |
| .badge::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| border-radius: 20px; | |
| padding: 3px; | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); | |
| mask-composite: exclude; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: var(--transition); | |
| } | |
| .badge:hover::before { | |
| opacity: 1; | |
| } | |
| .badge.earned { | |
| border-color: var(--accent); | |
| box-shadow: 0 10px 30px rgba(255, 0, 255, 0.2); | |
| } | |
| .badge.earned::before { | |
| opacity: 1; | |
| } | |
| .badge.locked { | |
| opacity: 0.5; | |
| filter: grayscale(0.8); | |
| } | |
| .badge-icon { | |
| font-size: 3.5rem; | |
| margin-bottom: 15px; | |
| display: block; | |
| filter: drop-shadow(0 0 10px currentColor); | |
| } | |
| .badge-name { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| color: var(--text); | |
| } | |
| .badge-desc { | |
| font-family: 'Exo 2', sans-serif; | |
| font-size: 0.85rem; | |
| color: var(--muted); | |
| line-height: 1.4; | |
| } | |
| /* === HISTORY === */ | |
| .history-list { | |
| max-height: 500px; | |
| overflow-y: auto; | |
| padding-right: 15px; | |
| } | |
| .history-list::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .history-list::-webkit-scrollbar-track { | |
| background: rgba(0, 243, 255, 0.05); | |
| border-radius: 4px; | |
| } | |
| .history-list::-webkit-scrollbar-thumb { | |
| background: var(--primary); | |
| border-radius: 4px; | |
| } | |
| .history-item { | |
| padding: 25px; | |
| margin-bottom: 15px; | |
| background: rgba(26, 26, 58, 0.4); | |
| border-radius: 20px; | |
| border-left: 5px solid var(--success); | |
| transition: var(--transition); | |
| cursor: pointer; | |
| } | |
| .history-item:hover { | |
| transform: translateX(8px); | |
| background: rgba(26, 26, 58, 0.6); | |
| } | |
| .history-item.hint { | |
| border-left-color: var(--secondary); | |
| } | |
| .history-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| } | |
| .history-word { | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 600; | |
| font-size: 1.3rem; | |
| color: var(--text); | |
| } | |
| .history-stats { | |
| font-size: 1rem; | |
| color: var(--muted); | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| font-family: 'Rajdhani', sans-serif; | |
| } | |
| .history-details { | |
| font-size: 0.9rem; | |
| color: var(--muted); | |
| display: flex; | |
| gap: 20px; | |
| font-family: 'Exo 2', sans-serif; | |
| } | |
| /* === LOADING === */ | |
| .loader { | |
| display: inline-block; | |
| width: 30px; | |
| height: 30px; | |
| border: 4px solid rgba(0, 243, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: var(--primary); | |
| animation: neuralSpin 1s linear infinite; | |
| filter: drop-shadow(0 0 8px var(--primary)); | |
| } | |
| @keyframes neuralSpin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* === RESPONSIVE === */ | |
| @media (max-width: 1200px) { | |
| .app-container { | |
| padding: 20px; | |
| } | |
| .header-title { | |
| font-size: 3.5rem; | |
| } | |
| .header-subtitle { | |
| font-size: 1.2rem; | |
| } | |
| .panel { | |
| padding: 30px; | |
| } | |
| } | |
| @media (max-width: 900px) { | |
| .header-title { | |
| font-size: 2.8rem; | |
| letter-spacing: 3px; | |
| } | |
| .header-subtitle { | |
| font-size: 1.1rem; | |
| } | |
| .tab { | |
| min-width: 160px; | |
| padding: 20px; | |
| font-size: 0.9rem; | |
| } | |
| .stats { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| .progress-container { | |
| min-width: 100%; | |
| } | |
| .audio-controls-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .action-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .dashboard-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .badge-grid { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| .header-title { | |
| font-size: 2.2rem; | |
| } | |
| .header-subtitle { | |
| font-size: 1rem; | |
| } | |
| .tab { | |
| min-width: 140px; | |
| padding: 15px 10px; | |
| font-size: 0.8rem; | |
| } | |
| .stats { | |
| grid-template-columns: 1fr; | |
| gap: 15px; | |
| } | |
| .stat-value { | |
| font-size: 2.2rem; | |
| } | |
| .word-display { | |
| font-size: 2rem; | |
| letter-spacing: 8px; | |
| } | |
| .word-context { | |
| font-size: 1.2rem; | |
| } | |
| .answer-input { | |
| font-size: 1.4rem; | |
| padding: 20px; | |
| } | |
| .btn { | |
| padding: 15px 20px; | |
| font-size: 0.9rem; | |
| } | |
| .modal-content { | |
| padding: 30px 20px; | |
| } | |
| .modal-title { | |
| font-size: 2rem; | |
| } | |
| .badge-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* === UTILITY === */ | |
| .text-center { text-align: center; } | |
| .mt-1 { margin-top: 10px; } | |
| .mt-2 { margin-top: 20px; } | |
| .mt-3 { margin-top: 30px; } | |
| .mt-4 { margin-top: 40px; } | |
| .mb-1 { margin-bottom: 10px; } | |
| .mb-2 { margin-bottom: 20px; } | |
| .mb-3 { margin-bottom: 30px; } | |
| .mb-4 { margin-bottom: 40px; } | |
| .p-1 { padding: 10px; } | |
| .p-2 { padding: 20px; } | |
| .p-3 { padding: 30px; } | |
| .p-4 { padding: 40px; } | |
| .hidden { display: none; } | |
| .fade-in { animation: fadeIn 0.6s ease; } | |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- BACKGROUND EFFECTS --> | |
| <div class="neural-network"></div> | |
| <div class="synapse-layer"></div> | |
| <div class="neuron-grid" id="neuronGrid"></div> | |
| <div class="data-stream"></div> | |
| <!-- CELEBRATION EFFECTS --> | |
| <div class="celebration-container" id="celebrationContainer"></div> | |
| <!-- TOAST NOTIFICATIONS --> | |
| <div class="toast-container" id="toastContainer"></div> | |
| <!-- SESSION COMPLETE MODAL --> | |
| <div class="modal" id="sessionCompleteModal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h2 class="modal-title">🎉 SESSION COMPLETE!</h2> | |
| <button class="modal-close" id="modalCloseBtn">×</button> | |
| </div> | |
| <div id="modalContent"></div> | |
| </div> | |
| </div> | |
| <!-- MAIN APP --> | |
| <div class="app-container"> | |
| <!-- HEADER --> | |
| <header class="neural-header"> | |
| <h1 class="header-title">SPELLING TRAINER</h1> | |
| <p class="header-subtitle">AI-Powered Spelling Practice • Google TTS • Smart Learning</p> | |
| </header> | |
| <!-- MAIN GRID --> | |
| <div class="neural-grid"> | |
| <!-- SIDEBAR --> | |
| <aside class="neural-sidebar"> | |
| <!-- USER PROFILE --> | |
| <div class="sidebar-section"> | |
| <h3 class="section-title"><i class="fas fa-user-astronaut"></i> USER PROFILE</h3> | |
| <div class="input-group"> | |
| <label class="label">Name</label> | |
| <input type="text" id="userName" class="input" placeholder="Enter your name" value="Spelling Learner"> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label">Level</label> | |
| <select id="userLevel" class="select"> | |
| <option value="initiate">🧠 Beginner</option> | |
| <option value="adept">⚡ Intermediate</option> | |
| <option value="master">👑 Advanced</option> | |
| <option value="architect">🏛️ Expert</option> | |
| </select> | |
| </div> | |
| <button id="saveProfileBtn" class="btn secondary mt-2"> | |
| <i class="fas fa-save"></i> Save Profile | |
| </button> | |
| </div> | |
| <!-- TRAINING SETTINGS --> | |
| <div class="sidebar-section"> | |
| <h3 class="section-title"><i class="fas fa-cogs"></i> TRAINING SETTINGS</h3> | |
| <div class="input-group"> | |
| <label class="label">Training Mode</label> | |
| <select id="trainingMode" class="select"> | |
| <option value="learning">📚 Learn in Order</option> | |
| <option value="practice">🎯 Random Practice</option> | |
| <option value="sentence">💬 Use in Sentences</option> | |
| <option value="time">⏱️ Timed Challenge</option> | |
| <option value="trini">🌴 Advanced Mode</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label">Difficulty Level</label> | |
| <select id="difficultyLevel" class="select"> | |
| <option value="grade1">Grade 1: Basic</option> | |
| <option value="grade2">Grade 2: Easy</option> | |
| <option value="grade3">Grade 3: Intermediate</option> | |
| <option value="grade4">Grade 4: Advanced</option> | |
| <option value="grade5">Grade 5: Expert</option> | |
| <option value="grade6">Grade 6: Master</option> | |
| <option value="advanced">Grade 7: Challenge</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label">Session Length: <span id="sessionLengthValue">15</span> words</label> | |
| <input type="range" id="sessionLength" min="1" max="6" step="1" value="2" style="width: 100%;"> | |
| <div style="color: var(--muted); font-size: 0.9rem; text-align: center; margin-top: 10px;"> | |
| <span>10</span> | |
| <span style="margin-left: 20px;">20</span> | |
| <span style="margin-left: 20px;">30</span> | |
| <span style="margin-left: 20px;">40</span> | |
| <span style="margin-left: 20px;">50</span> | |
| <span style="margin-left: 20px;">All</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GOOGLE TTS CONFIG --> | |
| <div class="tts-config"> | |
| <h3 class="section-title"><i class="fab fa-google"></i> GOOGLE TTS</h3> | |
| <p style="color: var(--muted); font-size: 0.9rem; margin-bottom: 20px; line-height: 1.5;"> | |
| Premium voice synthesis. Get free API key from | |
| <a href="https://console.cloud.google.com" target="_blank" style="color: var(--primary);">Google Cloud Console</a> | |
| (1M characters free/month). | |
| </p> | |
| <div class="input-group"> | |
| <label class="label">API Key</label> | |
| <input type="password" id="googleApiKey" class="input" placeholder="Enter Google Cloud API key"> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label">Voice</label> | |
| <select id="googleVoice" class="select"> | |
| <!-- US English Neural2 Voices --> | |
| <optgroup label="🇺🇸 US English - Neural2 (Premium)"> | |
| <option value="en-US-Neural2-A">American Female - Natural Tone</option> | |
| <option value="en-US-Neural2-B">American Male - Warm Tone</option> | |
| <option value="en-US-Neural2-C">American Female - Clear Articulation</option> | |
| <option value="en-US-Neural2-D">American Male - Confident Tone</option> | |
| <option value="en-US-Neural2-E">American Female - Friendly Tone</option> | |
| <option value="en-US-Neural2-F">American Female - Professional Tone</option> | |
| <option value="en-US-Neural2-G">American Male - Authoritative Tone</option> | |
| <option value="en-US-Neural2-H">American Female - Calm Tone</option> | |
| <option value="en-US-Neural2-I">American Male - Energetic Tone</option> | |
| <option value="en-US-Neural2-J">American Female - Expressive Tone</option> | |
| </optgroup> | |
| <!-- US English Wavenet Voices --> | |
| <optgroup label="🇺🇸 US English - Wavenet"> | |
| <option value="en-US-Wavenet-A">American Female - Standard Voice</option> | |
| <option value="en-US-Wavenet-B">American Male - Standard Voice</option> | |
| <option value="en-US-Wavenet-C">American Female - Soft Tone</option> | |
| <option value="en-US-Wavenet-D">American Male - Deep Tone</option> | |
| <option value="en-US-Wavenet-E">American Female - Warm Tone</option> | |
| <option value="en-US-Wavenet-F">American Female - Clear Voice</option> | |
| <option value="en-US-Wavenet-G">American Male - Strong Tone</option> | |
| <option value="en-US-Wavenet-H">American Female - Gentle Tone</option> | |
| <option value="en-US-Wavenet-I">American Male - Friendly Tone</option> | |
| <option value="en-US-Wavenet-J">American Female - Professional Voice</option> | |
| </optgroup> | |
| <!-- US English Standard Voices --> | |
| <optgroup label="🇺🇸 US English - Standard"> | |
| <option value="en-US-Standard-A">American Female - Basic Voice</option> | |
| <option value="en-US-Standard-B">American Male - Basic Voice</option> | |
| <option value="en-US-Standard-C">American Female - Standard Tone</option> | |
| <option value="en-US-Standard-D">American Male - Standard Tone</option> | |
| <option value="en-US-Standard-E">American Female - Classic Voice</option> | |
| <option value="en-US-Standard-F">American Female - Traditional Tone</option> | |
| <option value="en-US-Standard-G">American Male - Classic Tone</option> | |
| <option value="en-US-Standard-H">American Female - Clear Pronunciation</option> | |
| <option value="en-US-Standard-I">American Male - Direct Tone</option> | |
| <option value="en-US-Standard-J">American Female - Simple Voice</option> | |
| </optgroup> | |
| <!-- UK English Neural2 Voices --> | |
| <optgroup label="🇬🇧 UK English - Neural2 (Premium)"> | |
| <option value="en-GB-Neural2-A">British Female - Received Pronunciation</option> | |
| <option value="en-GB-Neural2-B">British Male - Received Pronunciation</option> | |
| <option value="en-GB-Neural2-C">British Female - Modern Tone</option> | |
| <option value="en-GB-Neural2-D">British Male - Traditional Tone</option> | |
| </optgroup> | |
| <!-- UK English Wavenet Voices --> | |
| <optgroup label="🇬🇧 UK English - Wavenet"> | |
| <option value="en-GB-Wavenet-A">British Female - Standard Accent</option> | |
| <option value="en-GB-Wavenet-B">British Male - Standard Accent</option> | |
| <option value="en-GB-Wavenet-C">British Female - Formal Tone</option> | |
| <option value="en-GB-Wavenet-D">British Male - Formal Tone</option> | |
| </optgroup> | |
| <!-- UK English Standard Voices --> | |
| <optgroup label="🇬🇧 UK English - Standard"> | |
| <option value="en-GB-Standard-A">British Female - Basic Accent</option> | |
| <option value="en-GB-Standard-B">British Male - Basic Accent</option> | |
| <option value="en-GB-Standard-C">British Female - Classic Tone</option> | |
| <option value="en-GB-Standard-D">British Male - Classic Tone</option> | |
| </optgroup> | |
| <!-- Australian English Neural2 Voices --> | |
| <optgroup label="🇦🇺 Australian English - Neural2"> | |
| <option value="en-AU-Neural2-A">Australian Female - General Accent</option> | |
| <option value="en-AU-Neural2-B">Australian Male - General Accent</option> | |
| <option value="en-AU-Neural2-C">Australian Female - Friendly Tone</option> | |
| <option value="en-AU-Neural2-D">Australian Male - Casual Tone</option> | |
| </optgroup> | |
| <!-- Australian English Wavenet Voices --> | |
| <optgroup label="🇦🇺 Australian English - Wavenet"> | |
| <option value="en-AU-Wavenet-A">Australian Female - Standard Voice</option> | |
| <option value="en-AU-Wavenet-B">Australian Male - Standard Voice</option> | |
| <option value="en-AU-Wavenet-C">Australian Female - Clear Tone</option> | |
| <option value="en-AU-Wavenet-D">Australian Male - Warm Tone</option> | |
| </optgroup> | |
| <!-- Australian English Standard Voices --> | |
| <optgroup label="🇦🇺 Australian English - Standard"> | |
| <option value="en-AU-Standard-A">Australian Female - Basic Voice</option> | |
| <option value="en-AU-Standard-B">Australian Male - Basic Voice</option> | |
| <option value="en-AU-Standard-C">Australian Female - Natural Tone</option> | |
| <option value="en-AU-Standard-D">Australian Male - Natural Tone</option> | |
| </optgroup> | |
| <!-- Indian English Neural2 Voices --> | |
| <optgroup label="🇮🇳 Indian English - Neural2"> | |
| <option value="en-IN-Neural2-A">Indian Female - Standard Accent</option> | |
| <option value="en-IN-Neural2-B">Indian Male - Standard Accent</option> | |
| <option value="en-IN-Neural2-C">Indian Female - Clear Pronunciation</option> | |
| </optgroup> | |
| <!-- Indian English Wavenet Voices --> | |
| <optgroup label="🇮🇳 Indian English - Wavenet"> | |
| <option value="en-IN-Wavenet-A">Indian Female - Professional Tone</option> | |
| <option value="en-IN-Wavenet-B">Indian Male - Professional Tone</option> | |
| <option value="en-IN-Wavenet-C">Indian Female - Formal Tone</option> | |
| </optgroup> | |
| <!-- Indian English Standard Voices --> | |
| <optgroup label="🇮🇳 Indian English - Standard"> | |
| <option value="en-IN-Standard-A">Indian Female - Basic Tone</option> | |
| <option value="en-IN-Standard-B">Indian Male - Basic Tone</option> | |
| <option value="en-IN-Standard-C">Indian Female - Standard Pronunciation</option> | |
| </optgroup> | |
| <!-- Canadian English Neural2 Voices --> | |
| <optgroup label="🇨🇦 Canadian English - Neural2"> | |
| <option value="en-CA-Neural2-A">Canadian Female - Neutral Accent</option> | |
| <option value="en-CA-Neural2-B">Canadian Male - Neutral Accent</option> | |
| <option value="en-CA-Neural2-C">Canadian Female - Clear Tone</option> | |
| <option value="en-CA-Neural2-D">Canadian Male - Warm Tone</option> | |
| </optgroup> | |
| <!-- Canadian English Wavenet Voices --> | |
| <optgroup label="🇨🇦 Canadian English - Wavenet"> | |
| <option value="en-CA-Wavenet-A">Canadian Female - Standard Voice</option> | |
| <option value="en-CA-Wavenet-B">Canadian Male - Standard Voice</option> | |
| <option value="en-CA-Wavenet-C">Canadian Female - Friendly Tone</option> | |
| <option value="en-CA-Wavenet-D">Canadian Male - Professional Tone</option> | |
| </optgroup> | |
| <!-- Canadian English Standard Voices --> | |
| <optgroup label="🇨🇦 Canadian English - Standard"> | |
| <option value="en-CA-Standard-A">Canadian Female - Basic Voice</option> | |
| <option value="en-CA-Standard-B">Canadian Male - Basic Voice</option> | |
| <option value="en-CA-Standard-C">Canadian Female - Natural Tone</option> | |
| <option value="en-CA-Standard-D">Canadian Male - Natural Tone</option> | |
| </optgroup> | |
| </select> | |
| </div> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 25px;"> | |
| <button id="testGoogleBtn" class="btn secondary"> | |
| <i class="fas fa-play"></i> Test Voice | |
| </button> | |
| <button id="clearGoogleBtn" class="btn secondary"> | |
| <i class="fas fa-times"></i> Clear Key | |
| </button> | |
| </div> | |
| </div> | |
| <!-- QUICK STATS --> | |
| <div class="sidebar-section"> | |
| <h3 class="section-title"><i class="fas fa-chart-network"></i> QUICK STATS</h3> | |
| <div id="quickStats"> | |
| <!-- Dynamic stats --> | |
| </div> | |
| </div> | |
| <!-- SESSION CONTROLS --> | |
| <div class="sidebar-section"> | |
| <h3 class="section-title"><i class="fas fa-brain"></i> SESSION CONTROLS</h3> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;"> | |
| <button id="saveSessionBtn" class="btn secondary"> | |
| <i class="fas fa-save"></i> Save Progress | |
| </button> | |
| <button id="newSessionBtn" class="btn secondary"> | |
| <i class="fas fa-sync"></i> New Session | |
| </button> | |
| </div> | |
| <button id="exportDataBtn" class="btn secondary mt-2" style="width: 100%;"> | |
| <i class="fas fa-download"></i> Export Data | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- MAIN CONTENT --> | |
| <main class="neural-content"> | |
| <!-- TAB NAVIGATION --> | |
| <div class="tabs"> | |
| <button class="tab active" data-tab="game"> | |
| <i class="fas fa-brain-circuit"></i> Practice | |
| </button> | |
| <button class="tab" data-tab="analytics"> | |
| <i class="fas fa-chart-network"></i> Stats & Achievements | |
| </button> | |
| <button class="tab" data-tab="voices"> | |
| <i class="fas fa-wave-sine"></i> Voice Studio | |
| </button> | |
| <button class="tab" data-tab="settings"> | |
| <i class="fas fa-sliders-h"></i> Settings | |
| </button> | |
| </div> | |
| <!-- GAME PANEL --> | |
| <div class="panel active" id="game-panel"> | |
| <!-- GAME HEADER --> | |
| <div class="game-header"> | |
| <div class="progress-container"> | |
| <div style="flex: 1;"> | |
| <div class="progress"> | |
| <div class="progress-fill" id="progressFill" style="width: 20%;"></div> | |
| </div> | |
| <div style="text-align: center; margin-top: 15px; color: var(--muted); font-family: 'Rajdhani';" id="progressText"> | |
| Word 3/15 | |
| </div> | |
| </div> | |
| </div> | |
| <div class="stats"> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="scoreValue">0</div> | |
| <div class="stat-label">SCORE</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="streakValue">0</div> | |
| <div class="stat-label">STREAK</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="accuracyValue">0%</div> | |
| <div class="stat-label">ACCURACY</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="timeValue">0s</div> | |
| <div class="stat-label">TIME</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- WORD DISPLAY (HIDDEN CONTENT) --> | |
| <div class="word-display-container" id="wordDisplay"> | |
| <div style="text-align: center;"> | |
| <div class="word-display" id="currentWord">_ _ _ _ _ _ _ _</div> | |
| <div class="word-context" id="wordContext"> | |
| Loading... | |
| </div> | |
| </div> | |
| </div> | |
| <!-- AUDIO CONTROLS --> | |
| <div class="audio-controls-grid"> | |
| <button id="hearWordBtn" class="btn primary"> | |
| <i class="fas fa-volume-up"></i> Hear Word (1) | |
| </button> | |
| <button id="contextBtn" class="btn secondary"> | |
| <i class="fas fa-headphones"></i> Hear Context (2) | |
| </button> | |
| </div> | |
| <!-- INPUT AREA --> | |
| <div class="input-container"> | |
| <label class="input-label">ENTER THE SPELLING:</label> | |
| <input type="text" id="answerInput" class="answer-input" | |
| placeholder="Type the word... [ENTER] to submit" | |
| autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"> | |
| <div class="hint-text" id="hintText"> | |
| Hint: Press [ENTER] to submit or [TAB] for a hint | |
| </div> | |
| </div> | |
| <!-- ACTION BUTTONS --> | |
| <div class="action-grid"> | |
| <button id="checkAnswerBtn" class="btn success"> | |
| <i class="fas fa-check-circle"></i> Check Answer (ENTER) | |
| </button> | |
| <button id="hintBtn" class="btn warning"> | |
| <i class="fas fa-lightbulb"></i> Get Hint (H) | |
| </button> | |
| <button id="nextWordBtn" class="btn secondary" disabled> | |
| <i class="fas fa-arrow-right"></i> Next Word (N) | |
| </button> | |
| <button id="revealBtn" class="btn secondary"> | |
| <i class="fas fa-eye"></i> Reveal Answer (R) | |
| </button> | |
| </div> | |
| <!-- FEEDBACK --> | |
| <div class="feedback-container" id="feedbackContainer"></div> | |
| </div> | |
| <!-- ANALYTICS PANEL --> | |
| <div class="panel" id="analytics-panel"> | |
| <div class="dashboard-grid"> | |
| <!-- PERFORMANCE OVERVIEW --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-chart-network"></i> Performance Overview</h3> | |
| <div id="performanceChart"> | |
| <div style="height: 250px; display: flex; align-items: flex-end; gap: 15px; margin-top: 30px;"> | |
| <div style="flex: 1; display: flex; flex-direction: column; align-items: center;"> | |
| <div style="width: 85%; background: linear-gradient(to top, var(--success), #00ff9d); border-radius: 10px 10px 0 0; height: 85%;"></div> | |
| <div style="margin-top: 15px; color: var(--text); font-weight: 600; font-family: 'Rajdhani';">Correct</div> | |
| <div style="color: var(--muted); font-size: 0.9rem;">72%</div> | |
| </div> | |
| <div style="flex: 1; display: flex; flex-direction: column; align-items: center;"> | |
| <div style="width: 85%; background: linear-gradient(to top, var(--primary), #00f3ff); border-radius: 10px 10px 0 0; height: 60%;"></div> | |
| <div style="margin-top: 15px; color: var(--text); font-weight: 600; font-family: 'Rajdhani';">With Hint</div> | |
| <div style="color: var(--muted); font-size: 0.9rem;">18%</div> | |
| </div> | |
| <div style="flex: 1; display: flex; flex-direction: column; align-items: center;"> | |
| <div style="width: 85%; background: linear-gradient(to top, var(--warning), #ffd600); border-radius: 10px 10px 0 0; height: 25%;"></div> | |
| <div style="margin-top: 15px; color: var(--text); font-weight: 600; font-family: 'Rajdhani';">Incorrect</div> | |
| <div style="color: var(--muted); font-size: 0.9rem;">10%</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- BADGES COLLECTION --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-trophy"></i> Achievements</h3> | |
| <div class="badge-grid" id="badgesGrid"> | |
| <!-- Dynamic badges --> | |
| </div> | |
| </div> | |
| <!-- WORD HISTORY --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-history"></i> Recent Words</h3> | |
| <div class="history-list" id="wordHistory"> | |
| <!-- Dynamic history --> | |
| </div> | |
| </div> | |
| <!-- ACHIEVEMENT STATS --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-star"></i> Milestones</h3> | |
| <div id="achievementStats"> | |
| <!-- Dynamic stats --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- VOICE STUDIO PANEL --> | |
| <div class="panel" id="voices-panel"> | |
| <div class="dashboard-grid"> | |
| <!-- VOICE TESTING --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-flask"></i> Voice Testing</h3> | |
| <div style="margin-bottom: 30px;"> | |
| <label class="label">Text to Speak</label> | |
| <textarea id="testText" class="input" style="height: 150px; resize: vertical;">Welcome to Spelling Trainer. This system uses Google Text-to-Speech with premium voices for better learning.</textarea> | |
| </div> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 30px;"> | |
| <button id="testTtsBtn" class="btn primary"> | |
| <i class="fas fa-play"></i> Speak | |
| </button> | |
| <button id="stopTtsBtn" class="btn secondary"> | |
| <i class="fas fa-stop"></i> Stop | |
| </button> | |
| <button id="downloadAudioBtn" class="btn secondary"> | |
| <i class="fas fa-download"></i> Download Audio | |
| </button> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label">Voice Settings</label> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;"> | |
| <div> | |
| <div style="color: var(--muted); font-size: 0.9rem; margin-bottom: 8px;">Speed</div> | |
| <input type="range" id="speechRate" min="0.5" max="2" step="0.1" value="1" style="width: 100%;"> | |
| <div style="text-align: center; color: var(--primary); margin-top: 8px; font-family: 'Orbitron';" id="rateValue">1.0x</div> | |
| </div> | |
| <div> | |
| <div style="color: var(--muted); font-size: 0.9rem; margin-bottom: 8px;">Pitch</div> | |
| <input type="range" id="speechPitch" min="0.5" max="2" step="0.1" value="1" style="width: 100%;"> | |
| <div style="text-align: center; color: var(--primary); margin-top: 8px; font-family: 'Orbitron';" id="pitchValue">1.0</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- VOICE GALLERY --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-users"></i> Voice Gallery</h3> | |
| <div id="voiceGallery" style="margin-top: 30px;"> | |
| <!-- Dynamic voice gallery --> | |
| </div> | |
| </div> | |
| <!-- VOICE COMPARISON --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-balance-scale"></i> Voice Comparison</h3> | |
| <div id="voiceComparison"> | |
| <!-- Dynamic comparison --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SETTINGS PANEL --> | |
| <div class="panel" id="settings-panel"> | |
| <div class="dashboard-grid"> | |
| <!-- GAME SETTINGS --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-gamepad"></i> Game Settings</h3> | |
| <div class="input-group"> | |
| <label class="label" style="display: flex; align-items: center; gap: 15px;"> | |
| <input type="checkbox" id="autoPlayCheck" checked style="transform: scale(1.2);"> | |
| Play word automatically | |
| </label> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label" style="display: flex; align-items: center; gap: 15px;"> | |
| <input type="checkbox" id="soundEffectsCheck" checked style="transform: scale(1.2);"> | |
| Enable sound effects | |
| </label> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label" style="display: flex; align-items: center; gap: 15px;"> | |
| <input type="checkbox" id="animationsCheck" checked style="transform: scale(1.2);"> | |
| Enable animations | |
| </label> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label" style="display: flex; align-items: center; gap: 15px;"> | |
| <input type="checkbox" id="hintsEnabledCheck" checked style="transform: scale(1.2);"> | |
| Enable hints | |
| </label> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label">Difficulty Adjustment</label> | |
| <select id="difficultyAdjustment" class="select"> | |
| <option value="auto">Auto-adjust to performance</option> | |
| <option value="fixed">Fixed difficulty</option> | |
| <option value="progressive">Progressive increase</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- AUDIO SETTINGS --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-wave-sine"></i> Audio Settings</h3> | |
| <div class="input-group"> | |
| <label class="label">TTS Engine</label> | |
| <select id="ttsEngine" class="select"> | |
| <option value="google">🧬 Google TTS (Premium)</option> | |
| <option value="edge">⚡ Edge TTS (Fallback)</option> | |
| <option value="web-speech">🔄 Web Speech API (Basic)</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label">Voice Gender</label> | |
| <select id="voiceGender" class="select"> | |
| <option value="female">♀ Female</option> | |
| <option value="male">♂ Male</option> | |
| <option value="neutral">⚪ Neutral</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label class="label">Volume</label> | |
| <input type="range" id="masterVolume" min="0" max="1" step="0.1" value="0.8" style="width: 100%;"> | |
| <div style="text-align: center; color: var(--primary); margin-top: 8px; font-family: 'Orbitron';" id="volumeValue">80%</div> | |
| </div> | |
| </div> | |
| <!-- DATA MANAGEMENT --> | |
| <div class="dashboard-card"> | |
| <h3><i class="fas fa-database"></i> Data Management</h3> | |
| <div style="display: grid; grid-template-columns: 1fr; gap: 20px; margin-top: 30px;"> | |
| <button id="exportProgressBtn" class="btn secondary"> | |
| <i class="fas fa-file-export"></i> Export Progress | |
| </button> | |
| <button id="importProgressBtn" class="btn secondary"> | |
| <i class="fas fa-file-import"></i> Import Data | |
| </button> | |
| <button id="clearDataBtn" class="btn secondary"> | |
| <i class="fas fa-trash"></i> Clear Data | |
| </button> | |
| <button id="resetSettingsBtn" class="btn secondary"> | |
| <i class="fas fa-undo"></i> Reset Settings | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| <!-- MASTER ENGINE --> | |
| <script> | |
| // ========== STATE ARCHITECTURE ========== | |
| const AppState = { | |
| // Core Training State | |
| currentWordIndex: 0, | |
| totalWords: 15, | |
| score: 0, | |
| streak: 0, | |
| attempts: 0, | |
| hintsUsed: 0, | |
| currentWord: "", | |
| currentContext: "", | |
| maskedWord: "", | |
| revealedLetters: 0, | |
| isCorrect: false, | |
| isFirstTry: true, | |
| startTime: null, | |
| sessionTime: 0, | |
| // TTS Engine Configuration | |
| ttsEngine: 'google', | |
| googleApiKey: localStorage.getItem('googleApiKey') || '', | |
| googleVoice: localStorage.getItem('googleVoice') || 'en-US-Neural2-A', | |
| edgeVoice: 'en-US-AriaNeural', | |
| speechRate: 1.0, | |
| speechPitch: 1.0, | |
| speechVolume: 0.8, | |
| isSpeaking: false, | |
| // User Profile | |
| userName: localStorage.getItem('userName') || 'Spelling Learner', | |
| userLevel: localStorage.getItem('userLevel') || 'initiate', | |
| userXP: parseInt(localStorage.getItem('userXP')) || 0, | |
| // Word Database | |
| wordLists: { | |
| grade1: [], grade2: [], grade3: [], grade4: [], grade5: [], grade6: [], | |
| sentence1: [], sentence2: [], sentence3: [], time: [], trini: [] | |
| }, | |
| currentWordList: [], | |
| // Performance Metrics | |
| performance: { | |
| totalSessions: parseInt(localStorage.getItem('totalSessions')) || 0, | |
| totalWordsCompleted: parseInt(localStorage.getItem('totalWordsCompleted')) || 0, | |
| perfectFirstTries: parseInt(localStorage.getItem('perfectFirstTries')) || 0, | |
| hintCorrect: parseInt(localStorage.getItem('hintCorrect')) || 0, | |
| incorrect: parseInt(localStorage.getItem('incorrect')) || 0, | |
| totalTime: parseInt(localStorage.getItem('totalTime')) || 0, | |
| longestStreak: parseInt(localStorage.getItem('longestStreak')) || 0, | |
| currentStreak: 0 | |
| }, | |
| // Achievements | |
| badges: [ | |
| { id: 1, name: "Perfect 10", description: "10 words correct in a row", earned: false, icon: "🔟", rarity: "common" }, | |
| { id: 2, name: "Word Explorer", description: "50 words mastered", earned: false, icon: "🧭", rarity: "common" }, | |
| { id: 3, name: "Speed Learner", description: "<30s average per word", earned: false, icon: "⚡", rarity: "uncommon" }, | |
| { id: 4, name: "Accuracy Master", description: "90%+ accuracy", earned: false, icon: "🎯", rarity: "rare" }, | |
| { id: 5, name: "Perfect Session", description: "100% session accuracy", earned: false, icon: "💎", rarity: "epic" }, | |
| { id: 6, name: "Word Master", description: "100 words mastered", earned: false, icon: "🏛️", rarity: "legendary" }, | |
| { id: 7, name: "Long Streak", description: "20+ perfect streak", earned: false, icon: "🔥", rarity: "rare" }, | |
| { id: 8, name: "No Help", description: "Session without hints", earned: false, icon: "💪", rarity: "uncommon" }, | |
| { id: 9, name: "Very Fast", description: "<15s average per word", earned: false, icon: "⏱️", rarity: "epic" }, | |
| { id: 10, name: "7-Day Streak", description: "7-day learning streak", earned: false, icon: "📅", rarity: "rare" } | |
| ], | |
| // History | |
| history: JSON.parse(localStorage.getItem('wordHistory')) || [], | |
| sessionHistory: [], | |
| // Sound Effects | |
| sounds: { | |
| perfect: null, | |
| ding: null, | |
| tryAgain: null | |
| } | |
| }; | |
| // ========== INITIALIZATION ========== | |
| document.addEventListener('DOMContentLoaded', async function() { | |
| // Initialize visualization | |
| initVisualization(); | |
| // Load sound effects | |
| loadSoundEffects(); | |
| // Load word lists | |
| await loadWordLists(); | |
| // Initialize training session | |
| initTrainingSession(); | |
| // Initialize UI | |
| initUI(); | |
| // Initialize event handlers | |
| initEventHandlers(); | |
| // Load first word | |
| loadWord(); | |
| // Update interface | |
| updateInterface(); | |
| updateQuickStats(); | |
| renderBadges(); | |
| renderHistory(); | |
| // Display welcome | |
| showToast('System Ready', 'Google TTS ready. Training session started.', 'success'); | |
| // Start time tracking | |
| AppState.startTime = Date.now(); | |
| startTimeTracker(); | |
| }); | |
| // ========== SOUND EFFECTS ========== | |
| function loadSoundEffects() { | |
| try { | |
| AppState.sounds.perfect = new Audio('sounds/perfect.wav'); | |
| AppState.sounds.ding = new Audio('sounds/ding.wav'); | |
| AppState.sounds.tryAgain = new Audio('sounds/try_again.wav'); | |
| // Preload sounds | |
| AppState.sounds.perfect.load(); | |
| AppState.sounds.ding.load(); | |
| AppState.sounds.tryAgain.load(); | |
| console.log('Sound effects loaded successfully'); | |
| } catch (error) { | |
| console.warn('Could not load sound effects:', error); | |
| } | |
| } | |
| function playSound(soundName) { | |
| try { | |
| if (!AppState.sounds[soundName]) return; | |
| const sound = AppState.sounds[soundName]; | |
| sound.volume = AppState.speechVolume; | |
| sound.currentTime = 0; | |
| sound.play().catch(e => console.warn('Could not play sound:', e)); | |
| } catch (error) { | |
| console.warn('Error playing sound:', error); | |
| } | |
| } | |
| // ========== CELEBRATION EFFECTS ========== | |
| function createCelebration() { | |
| const container = document.getElementById('celebrationContainer'); | |
| container.innerHTML = ''; | |
| // Create confetti for all correct answers | |
| for (let i = 0; i < 25; i++) { | |
| createConfetti(i); | |
| } | |
| } | |
| function createConfetti(index) { | |
| const container = document.getElementById('celebrationContainer'); | |
| const confetti = document.createElement('div'); | |
| confetti.className = 'confetti'; | |
| const colors = [ | |
| 'var(--primary)', | |
| 'var(--secondary)', | |
| 'var(--accent)', | |
| 'var(--success)', | |
| 'var(--warning)' | |
| ]; | |
| confetti.style.background = colors[Math.floor(Math.random() * colors.length)]; | |
| confetti.style.left = `${Math.random() * 100}%`; | |
| confetti.style.animationDelay = `${Math.random() * 1}s`; | |
| confetti.style.width = `${Math.random() * 8 + 4}px`; | |
| confetti.style.height = confetti.style.width; | |
| container.appendChild(confetti); | |
| setTimeout(() => { | |
| if (confetti.parentNode) { | |
| confetti.remove(); | |
| } | |
| }, 3000); | |
| } | |
| // ========== VISUALIZATION ENGINE ========== | |
| function initVisualization() { | |
| const neuronGrid = document.getElementById('neuronGrid'); | |
| const neuronCount = 50; | |
| for (let i = 0; i < neuronCount; i++) { | |
| const neuron = document.createElement('div'); | |
| neuron.style.position = 'absolute'; | |
| neuron.style.width = Math.random() * 8 + 2 + 'px'; | |
| neuron.style.height = neuron.style.width; | |
| neuron.style.background = `radial-gradient(circle, | |
| rgba(${Math.random() > 0.5 ? '0,243,255' : '157,0,255'}, ${Math.random() * 0.5 + 0.3}) 0%, | |
| transparent 70%)`; | |
| neuron.style.borderRadius = '50%'; | |
| neuron.style.left = Math.random() * 100 + '%'; | |
| neuron.style.top = Math.random() * 100 + '%'; | |
| neuron.style.filter = 'blur(1px)'; | |
| neuron.style.animation = `neuronPulse ${Math.random() * 4 + 2}s ease-in-out infinite`; | |
| neuron.style.animationDelay = Math.random() * 5 + 's'; | |
| neuronGrid.appendChild(neuron); | |
| } | |
| // Mouse interaction | |
| document.addEventListener('mousemove', (e) => { | |
| const x = (e.clientX / window.innerWidth) * 100; | |
| const y = (e.clientY / window.innerHeight) * 100; | |
| neuronGrid.style.setProperty('--x', `${x}%`); | |
| neuronGrid.style.setProperty('--y', `${y}%`); | |
| }); | |
| } | |
| // ========== WORD DATABASE LOADER ========== | |
| async function loadWordLists() { | |
| // Define file names for each difficulty level | |
| const fileMap = { | |
| grade1: 'grade1.txt', | |
| grade2: 'grade2.txt', | |
| grade3: 'grade3.txt', | |
| grade4: 'grade4.txt', | |
| grade5: 'grade5.txt', | |
| grade6: 'grade6.txt', | |
| sentence1: 'sentence1.txt', | |
| sentence2: 'sentence2.txt', | |
| sentence3: 'sentence3.txt', | |
| time: 'time.txt', | |
| trini: 'trini.txt' | |
| }; | |
| // Load each file from the "lists" folder | |
| for (const [grade, fileName] of Object.entries(fileMap)) { | |
| try { | |
| // Fetch the text file from the lists folder | |
| const response = await fetch(`lists/${fileName}`); | |
| if (!response.ok) { | |
| throw new Error(`Failed to load ${fileName}: ${response.status}`); | |
| } | |
| const text = await response.text(); | |
| AppState.wordLists[grade] = parseWordFile(text); | |
| console.log(`Loaded ${AppState.wordLists[grade].length} words from ${fileName}`); | |
| } catch (error) { | |
| console.error(`Error loading ${fileName}:`, error); | |
| // Fallback to empty array if file can't be loaded | |
| AppState.wordLists[grade] = []; | |
| showToast('File Error', `Could not load ${fileName}. Using empty word list.`, 'warning'); | |
| } | |
| } | |
| // Set initial word list | |
| AppState.currentWordList = AppState.wordLists.grade1; | |
| } | |
| // Helper function to parse the text file format | |
| function parseWordFile(text) { | |
| const lines = text.trim().split('\n'); | |
| const wordList = []; | |
| for (const line of lines) { | |
| // Skip empty lines | |
| if (!line.trim()) continue; | |
| // Split by '|' character | |
| const parts = line.split('|'); | |
| if (parts.length >= 2) { | |
| // Format: word|context | |
| wordList.push({ | |
| word: parts[0].trim(), | |
| context: parts[1].trim() | |
| }); | |
| } else if (parts.length === 1) { | |
| // Fallback: if no context provided, just use the word | |
| wordList.push({ | |
| word: parts[0].trim(), | |
| context: "" | |
| }); | |
| } | |
| } | |
| return wordList; | |
| } | |
| // ========== TRAINING SESSION ========== | |
| function initTrainingSession() { | |
| const mode = document.getElementById('trainingMode').value; | |
| const difficulty = document.getElementById('difficultyLevel').value; | |
| // Select appropriate word list | |
| let wordListKey = difficulty; | |
| if (mode === 'sentence') wordListKey = 'sentence1'; | |
| else if (mode === 'time') wordListKey = 'time'; | |
| else if (mode === 'trini') wordListKey = 'trini'; | |
| AppState.currentWordList = AppState.wordLists[wordListKey] || AppState.wordLists.grade1; | |
| // Set session duration based on slider | |
| const sliderValue = parseInt(document.getElementById('sessionLength').value); | |
| let sessionLength; | |
| // Map slider values: 1=10, 2=20, 3=30, 4=40, 5=50, 6=All | |
| if (sliderValue === 6) { | |
| // Use all words in the list | |
| sessionLength = AppState.currentWordList.length; | |
| } else { | |
| // 10, 20, 30, 40, or 50 words | |
| sessionLength = sliderValue * 10; | |
| } | |
| AppState.totalWords = Math.min(sessionLength, AppState.currentWordList.length); | |
| // Reset state | |
| AppState.currentWordIndex = 0; | |
| AppState.score = 0; | |
| AppState.streak = 0; | |
| AppState.sessionTime = 0; | |
| AppState.performance.currentStreak = 0; | |
| AppState.sessionHistory = []; | |
| // Randomize if in practice mode | |
| if (mode === 'practice') { | |
| shuffleArray(AppState.currentWordList); | |
| } | |
| // Update interface | |
| updateInterface(); | |
| } | |
| // ========== WORD PROCESSOR ========== | |
| function loadWord() { | |
| if (AppState.currentWordIndex >= AppState.currentWordList.length) { | |
| showCompletionModal(); | |
| return; | |
| } | |
| const wordData = AppState.currentWordList[AppState.currentWordIndex]; | |
| AppState.currentWord = wordData.word.toUpperCase(); | |
| AppState.currentContext = wordData.context; | |
| AppState.maskedWord = maskWord(AppState.currentWord, 0); | |
| AppState.revealedLetters = 0; | |
| AppState.isCorrect = false; | |
| AppState.isFirstTry = true; | |
| AppState.attempts = 0; | |
| AppState.hintsUsed = 0; | |
| // Update display (ALWAYS show masked word) | |
| document.getElementById('currentWord').textContent = AppState.maskedWord; | |
| document.getElementById('wordContext').textContent = AppState.currentContext || "Context not available."; | |
| document.getElementById('wordDisplay').className = 'word-display-container'; | |
| document.getElementById('feedbackContainer').className = 'feedback-container'; | |
| document.getElementById('answerInput').value = ''; | |
| document.getElementById('nextWordBtn').disabled = true; | |
| // Auto-play if enabled | |
| if (document.getElementById('autoPlayCheck')?.checked) { | |
| setTimeout(() => speakWord(), 600); | |
| } | |
| // Update progress | |
| updateProgress(); | |
| } | |
| // ========== GOOGLE TTS ENGINE ========== | |
| async function speakWord() { | |
| try { | |
| if (AppState.ttsEngine === 'google' && AppState.googleApiKey) { | |
| await speakWithGoogleTTS(AppState.currentWord); | |
| } else { | |
| await speakWithEdgeTTS(AppState.currentWord); | |
| } | |
| } catch (error) { | |
| console.error('Primary TTS failed, using fallback:', error); | |
| await speakWithEdgeTTS(AppState.currentWord); | |
| } | |
| } | |
| async function speakWithGoogleTTS(text) { | |
| if (!AppState.googleApiKey) { | |
| throw new Error('Google TTS not configured'); | |
| } | |
| try { | |
| const response = await fetch( | |
| `https://texttospeech.googleapis.com/v1/text:synthesize?key=${AppState.googleApiKey}`, | |
| { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| input: { text: text }, | |
| voice: { | |
| languageCode: AppState.googleVoice.substring(0, 5), // Extract language code (en-US, en-GB, etc.) | |
| name: AppState.googleVoice | |
| }, | |
| audioConfig: { | |
| audioEncoding: 'MP3', | |
| speakingRate: AppState.speechRate, | |
| pitch: AppState.speechPitch | |
| } | |
| }) | |
| } | |
| ); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(`Google TTS API error: ${JSON.stringify(errorData)}`); | |
| } | |
| const data = await response.json(); | |
| const audio = new Audio('data:audio/mp3;base64,' + data.audioContent); | |
| audio.volume = AppState.speechVolume; | |
| AppState.isSpeaking = true; | |
| audio.play(); | |
| return new Promise((resolve) => { | |
| audio.onended = () => { | |
| AppState.isSpeaking = false; | |
| resolve(); | |
| }; | |
| audio.onerror = () => { | |
| AppState.isSpeaking = false; | |
| resolve(); | |
| }; | |
| }); | |
| } catch (error) { | |
| console.error('Google TTS failed:', error); | |
| throw error; | |
| } | |
| } | |
| // ========== EDGE TTS ENGINE ========== | |
| async function speakWithEdgeTTS(text) { | |
| return new Promise((resolve) => { | |
| try { | |
| const encodedText = encodeURIComponent(text); | |
| const voice = AppState.edgeVoice; | |
| const rate = Math.round(AppState.speechRate * 100); | |
| const url = `https://edge.microsoft.com/tts/v1/speak?text=${encodedText}&voice=${voice}&rate=${rate}%`; | |
| const audio = new Audio(url); | |
| audio.volume = AppState.speechVolume; | |
| AppState.isSpeaking = true; | |
| audio.play(); | |
| audio.onended = () => { | |
| AppState.isSpeaking = false; | |
| resolve(); | |
| }; | |
| audio.onerror = () => { | |
| AppState.isSpeaking = false; | |
| resolve(); | |
| }; | |
| } catch (error) { | |
| console.error('Edge TTS failed:', error); | |
| AppState.isSpeaking = false; | |
| resolve(); | |
| } | |
| }); | |
| } | |
| // ========== VALIDATION ========== | |
| function validateAnswer() { | |
| const input = document.getElementById('answerInput'); | |
| const userAnswer = input.value.trim().toUpperCase(); | |
| if (!userAnswer) { | |
| showToast('No Input', 'Please enter a word.', 'warning'); | |
| return; | |
| } | |
| AppState.attempts++; | |
| if (userAnswer === AppState.currentWord) { | |
| processCorrectAnswer(); | |
| } else { | |
| processIncorrectAnswer(); | |
| } | |
| } | |
| function processCorrectAnswer() { | |
| AppState.isCorrect = true; | |
| AppState.isFirstTry = AppState.attempts === 1 && AppState.hintsUsed === 0; | |
| // REVEAL THE WORD FOR REINFORCEMENT | |
| document.getElementById('currentWord').textContent = AppState.currentWord.split('').join(' '); | |
| // Play sound effect | |
| if (AppState.isFirstTry) { | |
| playSound('perfect'); | |
| } else { | |
| playSound('ding'); | |
| } | |
| // Create visual celebration | |
| if (document.getElementById('animationsCheck')?.checked) { | |
| createCelebration(); | |
| } | |
| // Calculate points | |
| let points = 0; | |
| if (AppState.isFirstTry) { | |
| points = 10; | |
| AppState.streak++; | |
| AppState.performance.currentStreak++; | |
| showFeedback('🧠 Perfect! +10 points', 'correct'); | |
| } else { | |
| points = 5; | |
| AppState.streak = 0; | |
| showFeedback('✅ Correct! +5 points', 'correct'); | |
| } | |
| // Update score | |
| AppState.score += points; | |
| // Update performance | |
| AppState.performance.totalWordsCompleted++; | |
| if (AppState.isFirstTry) { | |
| AppState.performance.perfectFirstTries++; | |
| } else { | |
| AppState.performance.hintCorrect++; | |
| } | |
| // Update longest streak | |
| if (AppState.performance.currentStreak > AppState.performance.longestStreak) { | |
| AppState.performance.longestStreak = AppState.performance.currentStreak; | |
| } | |
| // Add to history | |
| const historyEntry = { | |
| word: AppState.currentWord, | |
| status: AppState.isFirstTry ? 'perfect' : 'hint', | |
| attempts: AppState.attempts, | |
| hints: AppState.hintsUsed, | |
| time: Math.floor((Date.now() - AppState.startTime) / 1000), | |
| timestamp: Date.now(), | |
| points: points | |
| }; | |
| AppState.history.unshift(historyEntry); | |
| AppState.sessionHistory.push(historyEntry); | |
| // Save to storage | |
| localStorage.setItem('wordHistory', JSON.stringify(AppState.history.slice(0, 100))); | |
| // Enable next word | |
| document.getElementById('nextWordBtn').disabled = false; | |
| // SHOW THE WORD - keep it revealed | |
| document.getElementById('wordDisplay').classList.add('correct'); | |
| // Update interface | |
| updateInterface(); | |
| updateQuickStats(); | |
| renderHistory(); | |
| // Check achievements | |
| checkAchievements(); | |
| // Auto-proceed | |
| setTimeout(() => { | |
| if (AppState.currentWordIndex < AppState.totalWords - 1) { | |
| document.getElementById('nextWordBtn').focus(); | |
| } | |
| }, 2000); | |
| } | |
| function processIncorrectAnswer() { | |
| AppState.isFirstTry = false; | |
| AppState.streak = 0; | |
| AppState.performance.currentStreak = 0; | |
| // Play try again sound | |
| playSound('tryAgain'); | |
| showFeedback('❌ Incorrect. Try again.', 'incorrect'); | |
| document.getElementById('wordDisplay').classList.add('incorrect'); | |
| // Update performance | |
| AppState.performance.incorrect++; | |
| // Clear input for retry | |
| setTimeout(() => { | |
| document.getElementById('answerInput').value = ''; | |
| document.getElementById('answerInput').focus(); | |
| }, 1000); | |
| updateInterface(); | |
| } | |
| // ========== HINT FUNCTIONS ========== | |
| function provideHint() { | |
| if (AppState.revealedLetters >= AppState.currentWord.length) return; | |
| AppState.revealedLetters++; | |
| AppState.hintsUsed++; | |
| // Update masked display | |
| AppState.maskedWord = maskWord(AppState.currentWord, AppState.revealedLetters); | |
| document.getElementById('currentWord').textContent = AppState.maskedWord; | |
| // Update interface | |
| updateInterface(); | |
| // Announce hint | |
| const revealedLetter = AppState.currentWord[AppState.revealedLetters - 1]; | |
| speakWithGoogleTTS(`Hint: Letter ${AppState.revealedLetters} is ${revealedLetter}`); | |
| } | |
| function revealAnswer() { | |
| AppState.revealedLetters = AppState.currentWord.length; | |
| AppState.maskedWord = maskWord(AppState.currentWord, AppState.revealedLetters); | |
| document.getElementById('currentWord').textContent = AppState.maskedWord; | |
| document.getElementById('hintBtn').disabled = true; | |
| speakWithGoogleTTS(`Answer revealed: ${AppState.currentWord}`); | |
| showToast('Answer Revealed', 'Answer shown. Try to remember it.', 'warning'); | |
| } | |
| // ========== INTERFACE UPDATER ========== | |
| function updateInterface() { | |
| // Update progress | |
| const progressPercent = (AppState.currentWordIndex / AppState.totalWords) * 100; | |
| document.getElementById('progressFill').style.width = `${progressPercent}%`; | |
| document.getElementById('progressText').textContent = | |
| `Word ${AppState.currentWordIndex + 1}/${AppState.totalWords}`; | |
| // Update statistics | |
| document.getElementById('scoreValue').textContent = AppState.score; | |
| document.getElementById('streakValue').textContent = AppState.streak; | |
| // Calculate accuracy | |
| const totalAttempts = AppState.performance.perfectFirstTries + | |
| AppState.performance.hintCorrect + | |
| AppState.performance.incorrect; | |
| const accuracy = totalAttempts > 0 ? | |
| Math.round(((AppState.performance.perfectFirstTries + AppState.performance.hintCorrect) / totalAttempts) * 100) : 0; | |
| document.getElementById('accuracyValue').textContent = `${accuracy}%`; | |
| // Update time | |
| document.getElementById('timeValue').textContent = `${Math.floor(AppState.sessionTime)}s`; | |
| // Update masked display if not correct | |
| if (!AppState.isCorrect) { | |
| document.getElementById('currentWord').textContent = AppState.maskedWord; | |
| } | |
| // Update controls | |
| document.getElementById('contextBtn').disabled = !AppState.currentContext; | |
| document.getElementById('hintBtn').disabled = AppState.isCorrect || | |
| AppState.revealedLetters >= AppState.currentWord.length; | |
| } | |
| // ========== UTILITIES ========== | |
| function maskWord(word, revealed) { | |
| return word.split('').map((char, index) => { | |
| if (char === ' ') return ' '; | |
| return index < revealed ? char : '_'; | |
| }).join(' '); | |
| } | |
| function shuffleArray(array) { | |
| for (let i = array.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [array[i], array[j]] = [array[j], array[i]]; | |
| } | |
| return array; | |
| } | |
| // ========== UI INITIALIZATION ========== | |
| function initUI() { | |
| // Set user configuration | |
| document.getElementById('userName').value = AppState.userName; | |
| document.getElementById('userLevel').value = AppState.userLevel; | |
| document.getElementById('googleApiKey').value = AppState.googleApiKey; | |
| document.getElementById('googleVoice').value = AppState.googleVoice; | |
| document.getElementById('ttsEngine').value = AppState.ttsEngine; | |
| // Initialize sliders | |
| document.getElementById('speechRate').value = AppState.speechRate; | |
| document.getElementById('speechPitch').value = AppState.speechPitch; | |
| document.getElementById('masterVolume').value = AppState.speechVolume; | |
| // Update slider displays | |
| document.getElementById('rateValue').textContent = `${AppState.speechRate.toFixed(1)}x`; | |
| document.getElementById('pitchValue').textContent = AppState.speechPitch.toFixed(1); | |
| document.getElementById('volumeValue').textContent = `${Math.round(AppState.speechVolume * 100)}%`; | |
| document.getElementById('sessionLengthValue').textContent = AppState.totalWords; | |
| } | |
| // ========== EVENT HANDLERS ========== | |
| function initEventHandlers() { | |
| // Tab navigation | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.addEventListener('click', function() { | |
| const panelId = this.getAttribute('data-tab'); | |
| activatePanel(panelId); | |
| }); | |
| }); | |
| // Game controls | |
| document.getElementById('hearWordBtn').addEventListener('click', () => speakWord()); | |
| document.getElementById('contextBtn').addEventListener('click', () => { | |
| if (AppState.currentContext) { | |
| speakWithGoogleTTS(AppState.currentContext); | |
| } | |
| }); | |
| // Validation | |
| document.getElementById('checkAnswerBtn').addEventListener('click', validateAnswer); | |
| document.getElementById('hintBtn').addEventListener('click', provideHint); | |
| document.getElementById('nextWordBtn').addEventListener('click', () => { | |
| AppState.currentWordIndex++; | |
| loadWord(); | |
| }); | |
| document.getElementById('revealBtn').addEventListener('click', revealAnswer); | |
| // Input handling | |
| document.getElementById('answerInput').addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| validateAnswer(); | |
| } | |
| }); | |
| // Session length slider | |
| document.getElementById('sessionLength').addEventListener('input', function() { | |
| const sliderValue = parseInt(this.value); | |
| let displayValue; | |
| // Map slider values: 1=10, 2=20, 3=30, 4=40, 5=50, 6=All | |
| if (sliderValue === 6) { | |
| displayValue = "All"; | |
| } else { | |
| displayValue = (sliderValue * 10).toString(); | |
| } | |
| document.getElementById('sessionLengthValue').textContent = displayValue; | |
| }); | |
| document.getElementById('sessionLength').addEventListener('change', function() { | |
| initTrainingSession(); | |
| loadWord(); | |
| }); | |
| // Google TTS configuration | |
| document.getElementById('testGoogleBtn').addEventListener('click', async () => { | |
| const apiKey = document.getElementById('googleApiKey').value; | |
| const voice = document.getElementById('googleVoice').value; | |
| if (!apiKey) { | |
| showToast('API Key Required', | |
| 'Enter Google Cloud API key for premium voice synthesis.', 'warning'); | |
| return; | |
| } | |
| AppState.googleApiKey = apiKey; | |
| AppState.googleVoice = voice; | |
| localStorage.setItem('googleApiKey', apiKey); | |
| localStorage.setItem('googleVoice', voice); | |
| try { | |
| await speakWithGoogleTTS("Google TTS ready. System online."); | |
| showToast('Google TTS Connected', 'Premium voice synthesis activated.', 'success'); | |
| } catch (error) { | |
| showToast('TTS Failed', | |
| 'Check API key. Using fallback TTS.', 'error'); | |
| } | |
| }); | |
| // Clear configuration | |
| document.getElementById('clearGoogleBtn').addEventListener('click', () => { | |
| document.getElementById('googleApiKey').value = ''; | |
| AppState.googleApiKey = ''; | |
| localStorage.removeItem('googleApiKey'); | |
| showToast('Settings Cleared', 'Google TTS settings cleared.', 'success'); | |
| }); | |
| // Training settings | |
| document.getElementById('trainingMode').addEventListener('change', () => { | |
| initTrainingSession(); | |
| loadWord(); | |
| }); | |
| document.getElementById('difficultyLevel').addEventListener('change', () => { | |
| initTrainingSession(); | |
| loadWord(); | |
| }); | |
| // Profile | |
| document.getElementById('saveProfileBtn').addEventListener('click', () => { | |
| AppState.userName = document.getElementById('userName').value; | |
| AppState.userLevel = document.getElementById('userLevel').value; | |
| localStorage.setItem('userName', AppState.userName); | |
| localStorage.setItem('userLevel', AppState.userLevel); | |
| showToast('Profile Saved', `Name: ${AppState.userName}`, 'success'); | |
| }); | |
| // Slider controls | |
| document.getElementById('speechRate').addEventListener('input', function() { | |
| AppState.speechRate = parseFloat(this.value); | |
| document.getElementById('rateValue').textContent = `${this.value}x`; | |
| }); | |
| document.getElementById('speechPitch').addEventListener('input', function() { | |
| AppState.speechPitch = parseFloat(this.value); | |
| document.getElementById('pitchValue').textContent = this.value; | |
| }); | |
| document.getElementById('masterVolume').addEventListener('input', function() { | |
| AppState.speechVolume = parseFloat(this.value); | |
| document.getElementById('volumeValue').textContent = `${Math.round(this.value * 100)}%`; | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', function(e) { | |
| if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { | |
| if (e.key !== 'Enter' && e.key !== 'Tab') return; | |
| } | |
| const shortcuts = ['1', '2', 'h', 'H', 'n', 'N', 'r', 'R', 'Enter']; | |
| if (shortcuts.includes(e.key)) { | |
| e.preventDefault(); | |
| } | |
| // Control mapping | |
| switch(e.key) { | |
| case '1': document.getElementById('hearWordBtn').click(); break; | |
| case '2': if (!document.getElementById('contextBtn').disabled) | |
| document.getElementById('contextBtn').click(); break; | |
| case 'h': | |
| case 'H': if (!document.getElementById('hintBtn').disabled) | |
| document.getElementById('hintBtn').click(); break; | |
| case 'n': | |
| case 'N': if (!document.getElementById('nextWordBtn').disabled) | |
| document.getElementById('nextWordBtn').click(); break; | |
| case 'r': | |
| case 'R': document.getElementById('revealBtn').click(); break; | |
| case 'Enter': | |
| if (!AppState.isCorrect) { | |
| document.getElementById('checkAnswerBtn').click(); | |
| } else if (!document.getElementById('nextWordBtn').disabled) { | |
| document.getElementById('nextWordBtn').click(); | |
| } | |
| break; | |
| case 'Tab': | |
| e.preventDefault(); | |
| if (!AppState.isCorrect) { | |
| document.getElementById('hintBtn').click(); | |
| } | |
| break; | |
| } | |
| }); | |
| } | |
| // ========== PANEL ACTIVATION ========== | |
| function activatePanel(panelId) { | |
| // Update tabs | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.remove('active'); | |
| if (tab.getAttribute('data-tab') === panelId) { | |
| tab.classList.add('active'); | |
| } | |
| }); | |
| // Update panels | |
| document.querySelectorAll('.panel').forEach(panel => { | |
| panel.classList.remove('active'); | |
| if (panel.id === `${panelId}-panel`) { | |
| panel.classList.add('active'); | |
| } | |
| }); | |
| // Panel-specific initialization | |
| if (panelId === 'analytics') { | |
| updateQuickStats(); | |
| renderHistory(); | |
| renderBadges(); | |
| } | |
| } | |
| // ========== TOAST SYSTEM ========== | |
| function showToast(title, message, type = 'info') { | |
| const toastContainer = document.getElementById('toastContainer'); | |
| const toastId = 'toast-' + Date.now(); | |
| const iconMap = { | |
| success: '✅', | |
| warning: '⚠️', | |
| error: '❌', | |
| info: 'ℹ️' | |
| }; | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.id = toastId; | |
| toast.innerHTML = ` | |
| <div class="toast-icon">${iconMap[type] || iconMap.info}</div> | |
| <div class="toast-content"> | |
| <div class="toast-title">${title}</div> | |
| <div class="toast-message">${message}</div> | |
| </div> | |
| <button class="toast-close" onclick="document.getElementById('${toastId}').remove()">×</button> | |
| `; | |
| toastContainer.appendChild(toast); | |
| // Auto-remove | |
| setTimeout(() => { | |
| const toastElement = document.getElementById(toastId); | |
| if (toastElement) { | |
| toastElement.style.opacity = '0'; | |
| toastElement.style.transform = 'translateX(100%)'; | |
| setTimeout(() => toastElement.remove(), 300); | |
| } | |
| }, 5000); | |
| } | |
| // ========== FEEDBACK SYSTEM ========== | |
| function showFeedback(message, type) { | |
| const feedback = document.getElementById('feedbackContainer'); | |
| feedback.textContent = message; | |
| feedback.className = `feedback-container ${type}`; | |
| setTimeout(() => { | |
| feedback.className = 'feedback-container'; | |
| }, 3000); | |
| } | |
| // ========== ACHIEVEMENT SYSTEM ========== | |
| function checkAchievements() { | |
| const achievements = AppState.badges.filter(badge => | |
| !badge.earned && validateAchievement(badge.id)); | |
| if (achievements.length > 0) { | |
| achievements.forEach(badge => { | |
| badge.earned = true; | |
| showToast('Achievement Unlocked', | |
| `${badge.icon} ${badge.name}: ${badge.description}`, 'success'); | |
| }); | |
| localStorage.setItem('badges', JSON.stringify(AppState.badges)); | |
| } | |
| } | |
| function validateAchievement(badgeId) { | |
| switch(badgeId) { | |
| case 1: return AppState.performance.currentStreak >= 10; | |
| case 2: return AppState.performance.totalWordsCompleted >= 50; | |
| case 3: return AppState.performance.totalWordsCompleted > 0 && | |
| (AppState.performance.totalTime / AppState.performance.totalWordsCompleted) < 30; | |
| case 4: return AppState.performance.totalWordsCompleted > 0 && | |
| (AppState.performance.perfectFirstTries / AppState.performance.totalWordsCompleted) >= 0.9; | |
| case 5: return AppState.sessionHistory.length > 0 && | |
| AppState.sessionHistory.every(item => item.status === 'perfect'); | |
| case 6: return AppState.performance.totalWordsCompleted >= 100; | |
| case 7: return AppState.performance.currentStreak >= 20; | |
| case 8: return AppState.sessionHistory.length > 0 && | |
| AppState.sessionHistory.every(item => item.hints === 0); | |
| case 9: return AppState.performance.totalWordsCompleted > 0 && | |
| (AppState.performance.totalTime / AppState.performance.totalWordsCompleted) < 15; | |
| default: return false; | |
| } | |
| } | |
| // ========== COMPLETION MODAL ========== | |
| function showCompletionModal() { | |
| const modal = document.getElementById('sessionCompleteModal'); | |
| const content = document.getElementById('modalContent'); | |
| // Calculate session metrics | |
| const perfectCount = AppState.sessionHistory.filter(item => item.status === 'perfect').length; | |
| const accuracy = AppState.sessionHistory.length > 0 ? | |
| Math.round((perfectCount / AppState.sessionHistory.length) * 100) : 0; | |
| content.innerHTML = ` | |
| <div style="text-align: center; padding: 20px 0;"> | |
| <div style="font-size: 5rem; margin-bottom: 30px; animation: neuralPulse 2s infinite;">🧠</div> | |
| <h3 style="color: var(--primary); margin-bottom: 20px; font-size: 2.5rem; font-family: 'Orbitron';">SESSION COMPLETE!</h3> | |
| <p style="margin-bottom: 40px; color: var(--muted); font-size: 1.3rem; max-width: 600px; margin: 0 auto 40px;"> | |
| You have completed ${AppState.totalWords} words. | |
| </p> | |
| <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 40px;"> | |
| <div class="stat-card" style="background: rgba(0, 255, 157, 0.1);"> | |
| <div class="stat-value" style="color: var(--success);">${AppState.score}</div> | |
| <div class="stat-label">SCORE</div> | |
| </div> | |
| <div class="stat-card" style="background: rgba(0, 243, 255, 0.1);"> | |
| <div class="stat-value" style="color: var(--primary);">${accuracy}%</div> | |
| <div class="stat-label">ACCURACY</div> | |
| </div> | |
| <div class="stat-card" style="background: rgba(255, 0, 255, 0.1);"> | |
| <div class="stat-value" style="color: var(--accent);">${perfectCount}</div> | |
| <div class="stat-label">PERFECT WORDS</div> | |
| </div> | |
| <div class="stat-card" style="background: rgba(255, 214, 0, 0.1);"> | |
| <div class="stat-value" style="color: var(--warning);">${Math.floor(AppState.sessionTime / 60)}m</div> | |
| <div class="stat-label">TIME SPENT</div> | |
| </div> | |
| </div> | |
| <div style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;"> | |
| <button id="newSessionModalBtn" class="btn primary" style="min-width: 200px;"> | |
| <i class="fas fa-sync"></i> New Session | |
| </button> | |
| <button id="reviewSessionBtn" class="btn secondary" style="min-width: 200px;"> | |
| <i class="fas fa-chart-network"></i> View Stats | |
| </button> | |
| <button id="shareResultsBtn" class="btn secondary" style="min-width: 200px;"> | |
| <i class="fas fa-share"></i> Export Data | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| modal.classList.add('active'); | |
| // Modal event handlers | |
| document.getElementById('newSessionModalBtn').addEventListener('click', () => { | |
| completeSession(); | |
| modal.classList.remove('active'); | |
| }); | |
| document.getElementById('reviewSessionBtn').addEventListener('click', () => { | |
| activatePanel('analytics'); | |
| modal.classList.remove('active'); | |
| }); | |
| document.getElementById('shareResultsBtn').addEventListener('click', exportData); | |
| } | |
| function completeSession() { | |
| // Update performance | |
| AppState.performance.totalSessions++; | |
| AppState.performance.totalTime += AppState.sessionTime; | |
| // Save data | |
| localStorage.setItem('totalSessions', AppState.performance.totalSessions); | |
| localStorage.setItem('totalWordsCompleted', AppState.performance.totalWordsCompleted); | |
| localStorage.setItem('perfectFirstTries', AppState.performance.perfectFirstTries); | |
| localStorage.setItem('hintCorrect', AppState.performance.hintCorrect); | |
| localStorage.setItem('incorrect', AppState.performance.incorrect); | |
| localStorage.setItem('totalTime', AppState.performance.totalTime); | |
| localStorage.setItem('longestStreak', AppState.performance.longestStreak); | |
| // Start new session | |
| initTrainingSession(); | |
| loadWord(); | |
| // Reset time tracking | |
| AppState.startTime = Date.now(); | |
| AppState.sessionTime = 0; | |
| showToast('New Session Started', 'New training session started.', 'success'); | |
| } | |
| // ========== DATA EXPORT ========== | |
| function exportData() { | |
| const data = { | |
| profile: { | |
| name: AppState.userName, | |
| level: AppState.userLevel, | |
| xp: AppState.userXP | |
| }, | |
| performance: AppState.performance, | |
| achievements: AppState.badges.filter(b => b.earned), | |
| history: AppState.history, | |
| timestamp: Date.now() | |
| }; | |
| const dataStr = JSON.stringify(data, null, 2); | |
| const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); | |
| const fileName = `spelling-data-${new Date().toISOString().split('T')[0]}.json`; | |
| const downloadLink = document.createElement('a'); | |
| downloadLink.setAttribute('href', dataUri); | |
| downloadLink.setAttribute('download', fileName); | |
| downloadLink.click(); | |
| showToast('Data Exported', 'Your progress data has been downloaded.', 'success'); | |
| } | |
| // ========== STATISTICS RENDERER ========== | |
| function updateQuickStats() { | |
| const container = document.getElementById('quickStats'); | |
| if (!container) return; | |
| const accuracy = AppState.performance.totalWordsCompleted > 0 ? | |
| Math.round((AppState.performance.perfectFirstTries / AppState.performance.totalWordsCompleted) * 100) : 0; | |
| container.innerHTML = ` | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 15px; align-items: center;"> | |
| <span style="color: var(--muted); font-size: 0.9rem;">Level:</span> | |
| <span style="color: var(--primary); font-weight: 600; font-size: 1rem; font-family: 'Orbitron';">${AppState.userLevel.toUpperCase()}</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 15px; align-items: center;"> | |
| <span style="color: var(--muted); font-size: 0.9rem;">XP:</span> | |
| <span style="color: var(--primary); font-weight: 600; font-size: 1rem; font-family: 'Orbitron';">${AppState.userXP}</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 15px; align-items: center;"> | |
| <span style="color: var(--muted); font-size: 0.9rem;">Words Completed:</span> | |
| <span style="color: var(--primary); font-weight: 600; font-size: 1rem; font-family: 'Orbitron';">${AppState.performance.totalWordsCompleted}</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 15px; align-items: center;"> | |
| <span style="color: var(--muted); font-size: 0.9rem;">Accuracy:</span> | |
| <span style="color: var(--primary); font-weight: 600; font-size: 1rem; font-family: 'Orbitron';">${accuracy}%</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <span style="color: var(--muted); font-size: 0.9rem;">Best Streak:</span> | |
| <span style="color: var(--primary); font-weight: 600; font-size: 1rem; font-family: 'Orbitron';">${AppState.performance.longestStreak}</span> | |
| </div> | |
| `; | |
| } | |
| // ========== BADGES RENDERER ========== | |
| function renderBadges() { | |
| const container = document.getElementById('badgesGrid'); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| // Load cached achievements | |
| const cachedBadges = JSON.parse(localStorage.getItem('badges')); | |
| if (cachedBadges) { | |
| AppState.badges = cachedBadges; | |
| } | |
| AppState.badges.forEach(badge => { | |
| const badgeElement = document.createElement('div'); | |
| badgeElement.className = `badge ${badge.earned ? 'earned' : 'locked'}`; | |
| badgeElement.innerHTML = ` | |
| <div class="badge-icon">${badge.icon}</div> | |
| <div class="badge-name">${badge.name}</div> | |
| <div class="badge-desc">${badge.description}</div> | |
| `; | |
| container.appendChild(badgeElement); | |
| }); | |
| } | |
| // ========== HISTORY RENDERER ========== | |
| function renderHistory() { | |
| const container = document.getElementById('wordHistory'); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| // Display recent 10 entries | |
| const recentHistory = AppState.history.slice(0, 10); | |
| if (recentHistory.length === 0) { | |
| container.innerHTML = ` | |
| <div style="text-align: center; padding: 60px 30px; color: var(--muted);"> | |
| <i class="fas fa-history" style="font-size: 3rem; margin-bottom: 20px; opacity: 0.5;"></i> | |
| <div style="font-size: 1.2rem; font-family: 'Rajdhani';">No history yet</div> | |
| <div style="font-size: 0.9rem; margin-top: 10px;">Complete a training session to see history</div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| recentHistory.forEach(item => { | |
| const historyElement = document.createElement('div'); | |
| historyElement.className = `history-item ${item.status === 'hint' ? 'hint' : ''}`; | |
| historyElement.innerHTML = ` | |
| <div class="history-header"> | |
| <div class="history-word">${'█'.repeat(item.word.length)}</div> | |
| <div class="history-stats"> | |
| <span style="color: ${item.status === 'perfect' ? 'var(--success)' : 'var(--secondary)'};"> | |
| ${item.status === 'perfect' ? '⭐' : '💡'} | |
| </span> | |
| <span>+${item.points}</span> | |
| </div> | |
| </div> | |
| <div class="history-details"> | |
| <span>${item.attempts} attempt${item.attempts > 1 ? 's' : ''}</span> | |
| <span>${item.hints} hint${item.hints !== 1 ? 's' : ''}</span> | |
| <span>${item.time}s</span> | |
| </div> | |
| `; | |
| container.appendChild(historyElement); | |
| }); | |
| } | |
| // ========== TIME TRACKER ========== | |
| function startTimeTracker() { | |
| setInterval(() => { | |
| if (AppState.startTime) { | |
| AppState.sessionTime = Math.floor((Date.now() - AppState.startTime) / 1000); | |
| document.getElementById('timeValue').textContent = `${AppState.sessionTime}s`; | |
| } | |
| }, 1000); | |
| } | |
| // ========== GLOBAL FUNCTIONS ========== | |
| window.showToast = showToast; | |
| window.activatePanel = activatePanel; | |
| </script> | |
| </body> | |
| </html> | |