Skip to main content

logicaffeine_web/ui/pages/
learn.rs

1//! Main learning interface.
2//!
3//! The primary learning experience with curriculum-driven exercises, real-time
4//! feedback, and progress tracking. Features include:
5//!
6//! - Module-based curriculum with progressive unlocking
7//! - Interactive exercises with immediate grading
8//! - Struggle detection and adaptive hints
9//! - Spaced repetition scheduling (SM-2)
10//! - XP rewards and achievement notifications
11//!
12//! # Layout
13//!
14//! - **Sidebar**: Module navigation with progress indicators
15//! - **Main content**: Exercise cards with reading material
16//! - **Reference panels**: Symbol dictionary and vocabulary
17//!
18//! # Route
19//!
20//! Accessed via [`Route::Learn`](crate::ui::router::Route::Learn).
21
22use dioxus::prelude::*;
23#[cfg(all(feature = "split", target_arch = "wasm32"))]
24use dioxus::wasm_split;
25use crate::ui::components::main_nav::{MainNav, ActivePage};
26use crate::ui::components::learn_sidebar::{LearnSidebar, ModuleInfo};
27use crate::ui::components::symbol_dictionary::SymbolDictionary;
28use crate::ui::components::vocab_reference::VocabReference;
29use crate::ui::components::guide_code_block::GuideCodeBlock;
30use crate::ui::components::footer::Footer;
31use crate::ui::components::icon::{Icon, IconVariant, IconSize};
32use crate::ui::pages::guide::content::ExampleMode;
33use crate::ui::seo::{JsonLdMultiple, PageHead, organization_schema, course_schema, breadcrumb_schema, BreadcrumbItem, pages as seo_pages};
34use crate::content::ContentEngine;
35use crate::generator::{Generator, AnswerType, Challenge};
36use crate::grader::check_answer;
37use crate::struggle::StruggleDetector;
38use crate::progress::UserProgress;
39use rand::SeedableRng;
40use rand::rngs::StdRng;
41use std::collections::HashSet;
42
43const LEARN_STYLE: &str = r#"
44.learn-page {
45    min-height: 100vh;
46    color: var(--text-primary);
47    background:
48        radial-gradient(1200px 600px at 50% -120px, rgba(167,139,250,0.14), transparent 60%),
49        radial-gradient(900px 500px at 15% 30%, rgba(96,165,250,0.14), transparent 60%),
50        radial-gradient(800px 450px at 90% 45%, rgba(34,197,94,0.08), transparent 62%),
51        linear-gradient(180deg, #070a12, #0b1022 55%, #070a12);
52    font-family: var(--font-sans);
53}
54
55/* Hero */
56.learn-hero {
57    max-width: 1280px;
58    margin: 0 auto;
59    padding: 60px var(--spacing-xl) 40px;
60}
61
62.learn-hero h1 {
63    font-size: var(--font-display-lg);
64    font-weight: 900;
65    letter-spacing: -1.5px;
66    line-height: 1.1;
67    background: linear-gradient(180deg, #ffffff 0%, rgba(229,231,235,0.78) 65%, rgba(229,231,235,0.62) 100%);
68    -webkit-background-clip: text;
69    -webkit-text-fill-color: transparent;
70    margin: 0 0 var(--spacing-lg);
71}
72
73.learn-hero p {
74    font-size: var(--font-body-lg);
75    color: var(--text-secondary);
76    max-width: 600px;
77    line-height: 1.6;
78    margin: 0;
79}
80
81.learn-hero-badge {
82    display: inline-flex;
83    align-items: center;
84    gap: var(--spacing-sm);
85    padding: var(--spacing-sm) 14px;
86    border-radius: var(--radius-full);
87    background: rgba(255,255,255,0.06);
88    border: 1px solid rgba(255,255,255,0.10);
89    font-size: var(--font-caption-md);
90    font-weight: 600;
91    color: var(--text-primary);
92    margin-bottom: var(--spacing-xl);
93}
94
95.learn-hero-badge .dot {
96    width: 8px;
97    height: 8px;
98    border-radius: 50%;
99    background: var(--color-success);
100    box-shadow: 0 0 0 4px rgba(34,197,94,0.15);
101}
102
103/* Layout */
104.learn-layout {
105    max-width: 1280px;
106    margin: 0 auto;
107    display: flex;
108    gap: 48px;
109    padding: 0 var(--spacing-xl) 80px;
110}
111
112/* Main content */
113.learn-content {
114    flex: 1;
115    min-width: 0;
116    max-width: 800px;
117}
118
119/* Era sections */
120.learn-era {
121    margin-bottom: 64px;
122    scroll-margin-top: 100px;
123}
124
125.learn-era-divider {
126    margin: 80px 0 48px;
127    padding: var(--spacing-xl) 0;
128    border-top: 1px solid rgba(255,255,255,0.08);
129}
130
131.learn-era-divider h2 {
132    font-size: var(--font-body-md);
133    font-weight: 700;
134    text-transform: uppercase;
135    letter-spacing: 1.5px;
136    color: var(--text-tertiary);
137    margin: 0;
138}
139
140.learn-era-header {
141    margin-bottom: var(--spacing-xl);
142    padding-bottom: var(--spacing-lg);
143    border-bottom: 1px solid rgba(255,255,255,0.08);
144}
145
146/* Collapsible era header styles */
147.learn-era-header.collapsible {
148    display: flex;
149    justify-content: space-between;
150    align-items: center;
151    padding: var(--spacing-md) var(--spacing-lg);
152    margin-bottom: var(--spacing-lg);
153    background: rgba(255,255,255,0.03);
154    border-radius: var(--radius-lg);
155    border-bottom: none;
156    cursor: pointer;
157    transition: background 0.2s ease;
158    user-select: none;
159}
160
161.learn-era-header.collapsible:hover {
162    background: rgba(255,255,255,0.06);
163}
164
165.learn-era-header.collapsible:focus-visible {
166    outline: 2px solid var(--color-accent-blue);
167    outline-offset: 2px;
168}
169
170.learn-era-header-content {
171    flex: 1;
172}
173
174.learn-era-chevron {
175    font-size: 14px;
176    color: var(--text-tertiary);
177    transition: transform 0.2s ease;
178    margin-left: var(--spacing-md);
179}
180
181.learn-era-chevron.collapsed {
182    transform: rotate(-90deg);
183}
184
185.learn-era-header h2 {
186    font-size: var(--font-display-md);
187    font-weight: 800;
188    letter-spacing: -0.8px;
189    line-height: 1.2;
190    background: linear-gradient(180deg, #ffffff 0%, rgba(229,231,235,0.85) 100%);
191    -webkit-background-clip: text;
192    -webkit-text-fill-color: transparent;
193    background-clip: text;
194    margin: 0 0 var(--spacing-sm);
195}
196
197.learn-era-header p {
198    color: var(--text-secondary);
199    font-size: var(--font-body-sm);
200    line-height: 1.6;
201    margin: 0;
202}
203
204/* Module cards */
205.learn-modules {
206    display: flex;
207    flex-direction: column;
208    gap: var(--spacing-xl);
209}
210
211.learn-module-card {
212    background: rgba(255,255,255,0.04);
213    border: 1px solid rgba(255,255,255,0.08);
214    border-radius: var(--radius-xl);
215    padding: var(--spacing-xl);
216    transition: all 0.2s ease;
217    scroll-margin-top: 100px;
218}
219
220.learn-module-card:hover {
221    background: rgba(255,255,255,0.06);
222    border-color: rgba(255,255,255,0.12);
223}
224
225.learn-module-header {
226    display: flex;
227    justify-content: space-between;
228    align-items: flex-start;
229    gap: var(--spacing-lg);
230    margin-bottom: var(--spacing-lg);
231}
232
233.learn-module-info {
234    flex: 1;
235}
236
237.learn-module-title {
238    font-size: var(--font-heading-sm);
239    font-weight: 700;
240    color: var(--text-primary);
241    margin: 0 0 6px;
242    display: flex;
243    align-items: center;
244    gap: 10px;
245}
246
247.learn-module-number {
248    font-size: var(--font-body-md);
249    font-weight: 700;
250    color: var(--color-accent-purple);
251    opacity: 0.8;
252}
253
254.learn-module-desc {
255    color: var(--text-secondary);
256    font-size: var(--font-body-md);
257    line-height: 1.5;
258    margin: 0;
259}
260
261.learn-module-meta {
262    display: flex;
263    flex-direction: column;
264    align-items: flex-end;
265    gap: var(--spacing-sm);
266}
267
268.learn-exercise-count {
269    font-size: var(--font-caption-md);
270    color: var(--text-secondary);
271    background: rgba(255,255,255,0.05);
272    padding: var(--spacing-xs) 10px;
273    border-radius: var(--radius-full);
274}
275
276.learn-difficulty {
277    display: flex;
278    gap: 3px;
279}
280
281.learn-difficulty-dot {
282    width: 8px;
283    height: 8px;
284    border-radius: 50%;
285    background: rgba(255,255,255,0.15);
286}
287
288.learn-difficulty-dot.filled {
289    background: linear-gradient(135deg, var(--color-accent-blue), var(--color-accent-purple));
290}
291
292/* Preview section */
293.learn-module-preview {
294    margin-top: var(--spacing-lg);
295    padding-top: var(--spacing-lg);
296    border-top: 1px solid rgba(255,255,255,0.06);
297}
298
299.learn-preview-label {
300    font-size: var(--font-caption-sm);
301    font-weight: 600;
302    text-transform: uppercase;
303    letter-spacing: 0.5px;
304    color: var(--text-tertiary);
305    margin-bottom: var(--spacing-md);
306}
307
308/* Action buttons */
309.learn-module-actions {
310    display: flex;
311    gap: 10px;
312    margin-top: var(--spacing-xl);
313}
314
315.learn-action-btn {
316    padding: 10px 18px;
317    border-radius: var(--radius-md);
318    font-size: var(--font-body-md);
319    font-weight: 600;
320    cursor: pointer;
321    transition: all 0.18s ease;
322    text-decoration: none;
323    display: inline-flex;
324    align-items: center;
325    gap: 6px;
326    border: 1px solid transparent;
327}
328
329.learn-action-btn.primary {
330    background: linear-gradient(135deg, rgba(96,165,250,0.9), rgba(167,139,250,0.9));
331    color: #060814;
332    border-color: rgba(255,255,255,0.1);
333}
334
335.learn-action-btn.primary:hover {
336    background: linear-gradient(135deg, var(--color-accent-blue), var(--color-accent-purple));
337}
338
339.learn-action-btn.secondary {
340    background: rgba(255,255,255,0.06);
341    color: var(--text-secondary);
342    border-color: rgba(255,255,255,0.12);
343}
344
345.learn-action-btn.secondary:hover {
346    background: rgba(255,255,255,0.10);
347    color: var(--text-primary);
348}
349
350/* Expanded module state */
351.learn-module-card.expanded {
352    background: rgba(255,255,255,0.08);
353    border-color: rgba(167,139,250,0.3);
354    box-shadow: 0 0 40px rgba(167,139,250,0.08);
355}
356
357.learn-module-expanded-content {
358    margin-top: var(--spacing-xl);
359    padding-top: var(--spacing-xl);
360    border-top: 1px solid rgba(255,255,255,0.08);
361}
362
363/* Mode selector inline card header */
364.mode-selector-header {
365    display: flex;
366    align-items: center;
367    justify-content: space-between;
368    margin-bottom: var(--spacing-xl);
369    padding: var(--spacing-lg);
370    background: rgba(255,255,255,0.03);
371    border: 1px solid rgba(255,255,255,0.08);
372    border-radius: var(--radius-lg);
373}
374
375.mode-selector-tabs {
376    display: flex;
377    gap: var(--spacing-sm);
378}
379
380.mode-tab-btn {
381    padding: 8px 16px;
382    border-radius: var(--radius-md);
383    font-size: var(--font-body-sm);
384    font-weight: 600;
385    cursor: pointer;
386    transition: all 0.18s ease;
387    border: 1px solid transparent;
388    background: rgba(255,255,255,0.05);
389    color: var(--text-secondary);
390}
391
392.mode-tab-btn:hover {
393    background: rgba(255,255,255,0.08);
394    color: var(--text-primary);
395}
396
397.mode-tab-btn.active {
398    background: linear-gradient(135deg, rgba(96,165,250,0.2), rgba(167,139,250,0.2));
399    border-color: rgba(96,165,250,0.3);
400    color: var(--color-accent-blue);
401}
402
403.mode-tab-btn.test {
404    background: rgba(251, 191, 36, 0.15);
405    border-color: rgba(251, 191, 36, 0.3);
406    color: #fbbf24;
407}
408
409.mode-stats {
410    display: flex;
411    gap: var(--spacing-lg);
412}
413
414.mode-stat {
415    display: flex;
416    align-items: center;
417    gap: var(--spacing-xs);
418    font-size: var(--font-body-sm);
419}
420
421.mode-stat-value {
422    font-weight: 700;
423    color: var(--color-accent-blue);
424}
425
426.mode-stat-value.streak {
427    color: #fbbf24;
428}
429
430.mode-stat-label {
431    color: var(--text-tertiary);
432}
433
434.combo-multiplier {
435    font-weight: 700;
436    color: #4ade80;
437    margin-left: 2px;
438}
439
440.mode-stat.combo {
441    background: rgba(251, 191, 36, 0.1);
442    padding: 4px 10px;
443    border-radius: var(--radius-md);
444    border: 1px solid rgba(251, 191, 36, 0.2);
445}
446
447/* Content tab selector */
448.content-tabs {
449    display: flex;
450    gap: var(--spacing-sm);
451    margin-bottom: var(--spacing-xl);
452    border-bottom: 1px solid rgba(255,255,255,0.08);
453    padding-bottom: var(--spacing-md);
454    /* Fix mobile overflow */
455    overflow-x: auto;
456    -webkit-overflow-scrolling: touch;
457    scrollbar-width: none;  /* Firefox */
458    -ms-overflow-style: none;  /* IE/Edge */
459}
460
461.content-tabs::-webkit-scrollbar {
462    display: none;  /* Chrome/Safari */
463}
464
465.content-tab-btn {
466    padding: 8px 16px;
467    border-radius: var(--radius-md) var(--radius-md) 0 0;
468    font-size: var(--font-body-sm);
469    font-weight: 600;
470    cursor: pointer;
471    transition: all 0.18s ease;
472    border: none;
473    background: transparent;
474    color: var(--text-tertiary);
475    border-bottom: 2px solid transparent;
476    margin-bottom: -1px;
477    /* Prevent shrinking on mobile */
478    flex-shrink: 0;
479    white-space: nowrap;
480}
481
482.content-tab-btn:hover {
483    color: var(--text-primary);
484}
485
486.content-tab-btn.active {
487    color: var(--color-accent-blue);
488    border-bottom-color: var(--color-accent-blue);
489}
490
491/* Practice tab - green accent */
492.content-tab-btn.practice {
493    color: var(--color-success);
494}
495
496.content-tab-btn.practice:hover {
497    color: var(--color-success);
498    background: rgba(74, 222, 128, 0.08);
499}
500
501.content-tab-btn.practice.active {
502    color: var(--color-success);
503    border-bottom-color: var(--color-success);
504    background: rgba(74, 222, 128, 0.1);
505}
506
507/* Test tab - orange/yellow accent */
508.content-tab-btn.test {
509    color: #fbbf24;
510}
511
512.content-tab-btn.test:hover {
513    color: #fbbf24;
514    background: rgba(251, 191, 36, 0.08);
515}
516
517.content-tab-btn.test.active {
518    color: #fbbf24;
519    border-bottom-color: #fbbf24;
520    background: rgba(251, 191, 36, 0.1);
521}
522
523/* Lesson section styling */
524.lesson-section {
525    margin-bottom: var(--spacing-xxl);
526    padding-bottom: var(--spacing-xl);
527    border-bottom: 1px solid rgba(255,255,255,0.06);
528}
529
530.lesson-section:last-child {
531    border-bottom: none;
532}
533
534.lesson-section-title {
535    font-size: 1.25rem;
536    font-weight: 700;
537    color: var(--text-primary);
538    margin-bottom: var(--spacing-lg);
539    display: flex;
540    align-items: center;
541    gap: var(--spacing-sm);
542}
543
544.lesson-section-number {
545    font-size: 1rem;
546    color: var(--color-accent-blue);
547    font-weight: 600;
548}
549
550.lesson-paragraph {
551    font-size: 1rem;
552    color: rgba(255, 255, 255, 0.85);
553    line-height: 1.75;
554    margin-bottom: var(--spacing-lg);
555}
556
557.lesson-definition {
558    background: rgba(167, 139, 250, 0.08);
559    border: 1px solid rgba(167, 139, 250, 0.2);
560    border-radius: var(--radius-md);
561    padding: var(--spacing-lg);
562    margin-bottom: var(--spacing-lg);
563}
564
565.lesson-definition-term {
566    font-size: 1rem;
567    font-weight: 700;
568    color: var(--color-accent-purple);
569    margin-bottom: var(--spacing-xs);
570}
571
572.lesson-definition-text {
573    font-size: 1rem;
574    color: rgba(255, 255, 255, 0.8);
575    line-height: 1.65;
576}
577
578.lesson-example {
579    background: rgba(96, 165, 250, 0.08);
580    border: 1px solid rgba(96, 165, 250, 0.2);
581    border-radius: var(--radius-md);
582    padding: var(--spacing-lg);
583    margin-bottom: var(--spacing-lg);
584}
585
586.lesson-example-title {
587    font-size: 1rem;
588    font-weight: 600;
589    color: var(--color-accent-blue);
590    margin-bottom: var(--spacing-md);
591}
592
593.lesson-example-premise {
594    font-size: 1rem;
595    color: rgba(255, 255, 255, 0.8);
596    padding-left: var(--spacing-lg);
597    margin-bottom: var(--spacing-xs);
598    line-height: 1.6;
599}
600
601.lesson-example-conclusion {
602    font-size: 1rem;
603    color: var(--text-primary);
604    font-weight: 500;
605    margin-top: var(--spacing-md);
606    padding-left: var(--spacing-lg);
607}
608
609.lesson-example-note {
610    margin-top: var(--spacing-md);
611    padding-top: var(--spacing-md);
612    border-top: 1px solid rgba(255,255,255,0.08);
613    color: rgba(255, 255, 255, 0.5);
614    font-size: 0.9rem;
615    font-style: italic;
616    line-height: 1.5;
617}
618
619/* Symbol glossary block */
620.lesson-symbols {
621    background: rgba(96, 165, 250, 0.08);
622    border: 1px solid rgba(96, 165, 250, 0.2);
623    border-radius: var(--radius-lg);
624    padding: var(--spacing-lg);
625    margin: var(--spacing-lg) 0;
626}
627
628.lesson-symbols-title {
629    font-weight: 700;
630    color: #60a5fa;
631    margin-bottom: var(--spacing-md);
632    font-size: 0.9rem;
633    text-transform: uppercase;
634    letter-spacing: 0.5px;
635}
636
637.lesson-symbols-grid {
638    display: grid;
639    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
640    gap: var(--spacing-md);
641}
642
643.lesson-symbol-item {
644    display: flex;
645    flex-direction: column;
646    gap: var(--spacing-sm);
647    padding: var(--spacing-md);
648    background: rgba(255,255,255,0.03);
649    border-radius: var(--radius-md);
650    text-align: center;
651}
652
653.lesson-symbol-glyph {
654    font-size: 2rem;
655    font-family: var(--font-mono);
656    color: #60a5fa;
657    text-align: center;
658}
659
660.lesson-symbol-name {
661    font-weight: 600;
662    color: var(--text-primary);
663    font-size: 0.9rem;
664}
665
666.lesson-symbol-meaning {
667    color: rgba(255, 255, 255, 0.6);
668    font-size: 0.85rem;
669}
670
671.lesson-symbol-example {
672    color: rgba(255, 255, 255, 0.5);
673    font-size: 0.8rem;
674    font-style: italic;
675    margin-top: 4px;
676}
677
678/* Quiz block */
679.lesson-quiz {
680    background: rgba(167, 139, 250, 0.08);
681    border: 1px solid rgba(167, 139, 250, 0.2);
682    border-radius: var(--radius-lg);
683    padding: var(--spacing-lg);
684    margin: var(--spacing-lg) 0;
685}
686
687.lesson-quiz-question {
688    font-weight: 600;
689    color: var(--text-primary);
690    margin-bottom: var(--spacing-md);
691    font-size: 1rem;
692}
693
694.lesson-quiz-options {
695    display: flex;
696    flex-direction: column;
697    gap: var(--spacing-sm);
698}
699
700.lesson-quiz-option {
701    text-align: left;
702    padding: var(--spacing-md);
703    background: rgba(255,255,255,0.04);
704    border: 1px solid rgba(255,255,255,0.1);
705    border-radius: var(--radius-md);
706    color: var(--text-secondary);
707    cursor: pointer;
708    transition: all 0.15s ease;
709}
710
711.lesson-quiz-option:hover:not(:disabled) {
712    background: rgba(255,255,255,0.08);
713    border-color: rgba(255,255,255,0.2);
714    color: var(--text-primary);
715}
716
717.lesson-quiz-option:disabled {
718    cursor: default;
719}
720
721.lesson-quiz-option.correct {
722    background: rgba(74, 222, 128, 0.15);
723    border-color: var(--color-success);
724    color: var(--color-success);
725}
726
727.lesson-quiz-option.incorrect {
728    background: rgba(248, 113, 113, 0.15);
729    border-color: var(--color-error);
730    color: var(--color-error);
731}
732
733.lesson-quiz-option.answered {
734    opacity: 0.5;
735}
736
737.lesson-quiz-explanation {
738    margin-top: var(--spacing-md);
739    padding-top: var(--spacing-md);
740    border-top: 1px solid rgba(255,255,255,0.08);
741    color: rgba(255, 255, 255, 0.7);
742    font-size: 0.9rem;
743    display: none;
744}
745
746.lesson-quiz-explanation.visible {
747    display: block;
748}
749
750.learn-module-close {
751    position: absolute;
752    top: var(--spacing-lg);
753    right: var(--spacing-lg);
754    width: 32px;
755    height: 32px;
756    border-radius: 50%;
757    background: rgba(255,255,255,0.08);
758    border: 1px solid rgba(255,255,255,0.12);
759    color: var(--text-secondary);
760    font-size: 18px;
761    cursor: pointer;
762    display: flex;
763    align-items: center;
764    justify-content: center;
765    transition: all 0.15s ease;
766}
767
768.learn-module-close:hover {
769    background: rgba(255,255,255,0.15);
770    color: var(--text-primary);
771}
772
773/* Tab content panels */
774.tab-panel {
775    padding: var(--spacing-xl) 0;
776}
777
778.tab-panel-lesson {
779    color: var(--text-primary);
780    line-height: 1.7;
781}
782
783.tab-panel-lesson h3 {
784    font-size: var(--font-heading-sm);
785    font-weight: 700;
786    margin: var(--spacing-xl) 0 var(--spacing-md);
787    color: var(--text-primary);
788}
789
790.tab-panel-lesson p {
791    margin-bottom: var(--spacing-lg);
792    color: var(--text-secondary);
793}
794
795/* Examples panel */
796.tab-panel-examples {
797    padding: var(--spacing-lg) 0;
798}
799
800.examples-intro {
801    margin-bottom: var(--spacing-xl);
802}
803
804.examples-intro h3 {
805    font-size: 1.25rem;
806    font-weight: 700;
807    color: var(--text-primary);
808    margin-bottom: var(--spacing-sm);
809}
810
811.examples-intro p {
812    font-size: 1rem;
813    color: rgba(255, 255, 255, 0.7);
814    line-height: 1.6;
815}
816
817.examples-list {
818    display: flex;
819    flex-direction: column;
820    gap: var(--spacing-xl);
821}
822
823.example-card {
824    background: rgba(255, 255, 255, 0.03);
825    border: 1px solid rgba(255, 255, 255, 0.08);
826    border-radius: var(--radius-lg);
827    padding: var(--spacing-lg);
828}
829
830.example-card .example-sentence {
831    font-size: 1rem;
832    font-weight: 500;
833    color: var(--text-primary);
834    margin-bottom: var(--spacing-md);
835    padding-bottom: var(--spacing-md);
836    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
837}
838
839.exercise-card {
840    background: rgba(255,255,255,0.04);
841    border: 1px solid rgba(255,255,255,0.08);
842    border-radius: var(--radius-lg);
843    padding: var(--spacing-xl);
844    margin-bottom: var(--spacing-lg);
845}
846
847.exercise-sentence {
848    font-size: 1.15rem;
849    font-weight: 500;
850    color: var(--text-primary);
851    margin-bottom: var(--spacing-lg);
852    line-height: 1.5;
853}
854
855.exercise-input-row {
856    display: flex;
857    gap: var(--spacing-md);
858}
859
860.exercise-input {
861    flex: 1;
862    padding: var(--spacing-md) var(--spacing-lg);
863    font-size: var(--font-body-md);
864    font-family: var(--font-mono);
865    background: rgba(255,255,255,0.06);
866    border: 2px solid rgba(255,255,255,0.12);
867    border-radius: var(--radius-md);
868    color: var(--text-primary);
869    outline: none;
870    transition: border-color 0.2s ease;
871}
872
873.exercise-input:focus {
874    border-color: var(--color-accent-blue);
875}
876
877.exercise-input.correct {
878    border-color: var(--color-success);
879    background: rgba(74, 222, 128, 0.1);
880}
881
882.exercise-input.incorrect {
883    border-color: var(--color-error);
884    background: rgba(248, 113, 113, 0.1);
885}
886
887.exercise-submit-btn {
888    padding: var(--spacing-md) var(--spacing-xl);
889    background: linear-gradient(135deg, var(--color-accent-blue), var(--color-accent-purple));
890    border: none;
891    border-radius: var(--radius-md);
892    color: #060814;
893    font-weight: 600;
894    cursor: pointer;
895    transition: opacity 0.15s ease;
896}
897
898.exercise-submit-btn:hover {
899    opacity: 0.9;
900}
901
902.exercise-feedback {
903    margin-top: var(--spacing-lg);
904    padding: var(--spacing-md);
905    border-radius: var(--radius-md);
906}
907
908.exercise-feedback.correct {
909    background: rgba(74, 222, 128, 0.15);
910    border: 1px solid rgba(74, 222, 128, 0.3);
911    color: var(--color-success);
912}
913
914.exercise-feedback.incorrect {
915    background: rgba(248, 113, 113, 0.15);
916    border: 1px solid rgba(248, 113, 113, 0.3);
917    color: var(--color-error);
918}
919
920.logic-output {
921    font-family: var(--font-mono);
922    font-size: var(--font-body-lg);
923    padding: var(--spacing-lg);
924    background: rgba(96, 165, 250, 0.1);
925    border: 1px solid rgba(96, 165, 250, 0.2);
926    border-radius: var(--radius-md);
927    color: var(--color-accent-blue);
928    margin: var(--spacing-lg) 0;
929}
930
931/* Responsive */
932@media (max-width: 1024px) {
933    .learn-layout {
934        flex-direction: column;
935    }
936
937    .learn-hero h1 {
938        font-size: var(--font-display-md);
939    }
940
941    .learn-hero {
942        padding: 40px var(--spacing-xl) var(--spacing-xxl);
943    }
944}
945
946/* Mobile breakpoint (phones) */
947@media (max-width: 768px) {
948    .content-tab-btn {
949        padding: 8px 12px;
950        font-size: var(--font-caption-lg, 13px);
951    }
952
953    .learn-main {
954        padding: var(--spacing-lg);
955    }
956}
957
958@media (max-width: 640px) {
959    .learn-hero h1 {
960        font-size: var(--font-heading-lg);
961    }
962
963    .learn-hero p {
964        font-size: var(--font-body-md);
965    }
966
967    .learn-era-header h2 {
968        font-size: var(--font-heading-lg);
969    }
970
971    .learn-module-header {
972        flex-direction: column;
973    }
974
975    .learn-module-meta {
976        flex-direction: row;
977        align-items: center;
978    }
979
980    .learn-module-actions {
981        flex-direction: column;
982    }
983
984    .learn-action-btn {
985        justify-content: center;
986    }
987
988    /* Smaller tabs on small phones */
989    .content-tab-btn {
990        padding: 6px 10px;
991        font-size: var(--font-caption-md, 12px);
992    }
993}
994"#;
995
996/// Module data with preview example
997struct ModuleData {
998    id: &'static str,
999    title: &'static str,
1000    description: &'static str,
1001    exercise_count: u32,
1002    difficulty: u8,
1003    preview_code: Option<&'static str>,
1004}
1005
1006/// Era data structure
1007struct EraData {
1008    id: &'static str,
1009    title: &'static str,
1010    description: &'static str,
1011    modules: Vec<ModuleData>,
1012}
1013
1014fn get_curriculum_data() -> Vec<EraData> {
1015    vec![
1016        // Era 1: First Steps
1017        EraData {
1018            id: "first-steps",
1019            title: "First Steps",
1020            description: "Get comfortable with logic. Learn what arguments are, how to spot good reasoning, and the classical foundations.",
1021            modules: vec![
1022                ModuleData {
1023                    id: "introduction",
1024                    title: "Introduction",
1025                    description: "Learn foundational concepts: what logic is, valid vs. invalid arguments, and sound reasoning.",
1026                    exercise_count: 5,
1027                    difficulty: 1,
1028                    preview_code: Some("All humans are mortal."),
1029                },
1030                ModuleData {
1031                    id: "syllogistic",
1032                    title: "Syllogistic Logic",
1033                    description: "Translate English into syllogistic notation. Master the classical form of logical reasoning.",
1034                    exercise_count: 98,
1035                    difficulty: 1,
1036                    preview_code: Some("All humans are mortal."),
1037                },
1038                ModuleData {
1039                    id: "definitions",
1040                    title: "Meaning and Definitions",
1041                    description: "Understand uses of language, types of definitions, and the analytic/synthetic distinction.",
1042                    exercise_count: 48,
1043                    difficulty: 2,
1044                    preview_code: None,
1045                },
1046                ModuleData {
1047                    id: "fallacies",
1048                    title: "Fallacies and Argumentation",
1049                    description: "Identify good arguments vs. fallacious reasoning. Master informal fallacies.",
1050                    exercise_count: 16,
1051                    difficulty: 2,
1052                    preview_code: None,
1053                },
1054                ModuleData {
1055                    id: "inductive",
1056                    title: "Inductive Reasoning",
1057                    description: "Master probability, analogical reasoning, Mill's methods, and inference to best explanation.",
1058                    exercise_count: 12,
1059                    difficulty: 2,
1060                    preview_code: Some("Most swans are white."),
1061                },
1062            ],
1063        },
1064        // Era 2: Building Blocks
1065        EraData {
1066            id: "building-blocks",
1067            title: "Building Blocks",
1068            description: "Master the core of formal logic. Propositional connectives, truth tables, and proof construction.",
1069            modules: vec![
1070                ModuleData {
1071                    id: "propositional",
1072                    title: "Basic Propositional Logic",
1073                    description: "Master AND, OR, NOT, and IF-THEN connectives. Truth tables, S-rules, and I-rules.",
1074                    exercise_count: 114,
1075                    difficulty: 2,
1076                    preview_code: Some("If John runs, then Mary walks."),
1077                },
1078                ModuleData {
1079                    id: "proofs",
1080                    title: "Propositional Proofs",
1081                    description: "Construct formal proofs and refutations. Learn natural deduction and truth trees.",
1082                    exercise_count: 14,
1083                    difficulty: 3,
1084                    preview_code: Some("1. P → Q  2. P  ∴ Q"),
1085                },
1086            ],
1087        },
1088        // Era 3: Expanding Horizons
1089        EraData {
1090            id: "expanding-horizons",
1091            title: "Expanding Horizons",
1092            description: "Explore richer logical systems. Quantifiers, modality, obligations, and beliefs.",
1093            modules: vec![
1094                ModuleData {
1095                    id: "quantificational",
1096                    title: "Basic Quantificational Logic",
1097                    description: "Master universal and existential quantifiers. Translations, proofs, and refutations.",
1098                    exercise_count: 12,
1099                    difficulty: 3,
1100                    preview_code: Some("All birds fly."),
1101                },
1102                ModuleData {
1103                    id: "relations",
1104                    title: "Relations and Identity",
1105                    description: "Extend predicate logic with identity and relations. Handle definite descriptions.",
1106                    exercise_count: 8,
1107                    difficulty: 3,
1108                    preview_code: Some("John loves Mary."),
1109                },
1110                ModuleData {
1111                    id: "modal",
1112                    title: "Basic Modal Logic",
1113                    description: "Explore possibility and necessity operators. Express what could be or must be true.",
1114                    exercise_count: 36,
1115                    difficulty: 3,
1116                    preview_code: Some("It is possible that John runs."),
1117                },
1118                ModuleData {
1119                    id: "further_modal",
1120                    title: "Further Modal Systems",
1121                    description: "Advanced modal systems including quantified modal logic and temporal operators.",
1122                    exercise_count: 2,
1123                    difficulty: 4,
1124                    preview_code: Some("John will run tomorrow."),
1125                },
1126                ModuleData {
1127                    id: "deontic",
1128                    title: "Deontic and Imperative Logic",
1129                    description: "Reason about obligation, permission, and prohibition. The logic of ethics and law.",
1130                    exercise_count: 38,
1131                    difficulty: 3,
1132                    preview_code: Some("John ought to leave."),
1133                },
1134                ModuleData {
1135                    id: "belief",
1136                    title: "Belief Logic",
1137                    description: "Express beliefs, knowledge, willing, and rationality. Model propositional attitudes.",
1138                    exercise_count: 15,
1139                    difficulty: 3,
1140                    preview_code: Some("John believes that Mary runs."),
1141                },
1142            ],
1143        },
1144        // Era 4: Mastery
1145        EraData {
1146            id: "mastery",
1147            title: "Mastery",
1148            description: "Deep understanding. The philosophy, history, and frontiers of logical thought.",
1149            modules: vec![
1150                ModuleData {
1151                    id: "ethics",
1152                    title: "A Formalized Ethical Theory",
1153                    description: "Apply logic to ethics: practical reason, consistency, and the golden rule formalized.",
1154                    exercise_count: 8,
1155                    difficulty: 4,
1156                    preview_code: None,
1157                },
1158                ModuleData {
1159                    id: "metalogic",
1160                    title: "Metalogic",
1161                    description: "Study logic about logic: soundness, completeness, and Gödel's incompleteness theorem.",
1162                    exercise_count: 6,
1163                    difficulty: 4,
1164                    preview_code: None,
1165                },
1166                ModuleData {
1167                    id: "history",
1168                    title: "History of Logic",
1169                    description: "Trace logic from Aristotle through Frege, Russell, and modern developments.",
1170                    exercise_count: 8,
1171                    difficulty: 2,
1172                    preview_code: None,
1173                },
1174                ModuleData {
1175                    id: "deviant",
1176                    title: "Deviant Logics",
1177                    description: "Explore non-classical logics: many-valued, paraconsistent, intuitionist, and relevance logic.",
1178                    exercise_count: 8,
1179                    difficulty: 4,
1180                    preview_code: None,
1181                },
1182                ModuleData {
1183                    id: "philosophy",
1184                    title: "Philosophy of Logic",
1185                    description: "Examine philosophical foundations: abstract entities, truth, paradoxes, and logic's scope.",
1186                    exercise_count: 8,
1187                    difficulty: 4,
1188                    preview_code: None,
1189                },
1190            ],
1191        },
1192    ]
1193}
1194
1195/// Expanded module key: (era_id, module_id)
1196type ExpandedModuleKey = Option<(String, String)>;
1197
1198#[component(lazy)]
1199pub fn Learn() -> Element {
1200    let mut active_module = use_signal(|| None::<String>);
1201    // Expanded module state: which module is currently expanded inline
1202    let mut expanded_module = use_signal::<ExpandedModuleKey>(|| None);
1203    // Collapsed eras state: tracks which eras are collapsed (all expanded by default)
1204    let mut collapsed_eras = use_signal(|| std::collections::HashSet::<String>::new());
1205
1206    let eras = get_curriculum_data();
1207
1208    // For module unlock checking
1209    let content_engine = ContentEngine::new();
1210    let user_progress = UserProgress::new(); // TODO: Load from storage
1211
1212    // Build module info for sidebar
1213    let sidebar_modules: Vec<ModuleInfo> = eras.iter().flat_map(|era| {
1214        era.modules.iter().map(|m| ModuleInfo {
1215            era_id: era.id.to_string(),
1216            era_title: era.title.to_string(),
1217            module_id: m.id.to_string(),
1218            module_title: m.title.to_string(),
1219            exercise_count: m.exercise_count,
1220            difficulty: m.difficulty,
1221        })
1222    }).collect();
1223
1224    // Collect all module IDs for intersection observer (used in wasm32 target)
1225    #[allow(unused_variables)]
1226    let module_ids: Vec<String> = eras.iter()
1227        .flat_map(|era| era.modules.iter().map(|m| m.id.to_string()))
1228        .collect();
1229
1230    // Set up scroll tracking with IntersectionObserver
1231    #[cfg(target_arch = "wasm32")]
1232    {
1233        use wasm_bindgen::prelude::*;
1234        use wasm_bindgen::JsCast;
1235
1236        let module_ids_for_effect = module_ids.clone();
1237
1238        use_effect(move || {
1239            let window = match web_sys::window() {
1240                Some(w) => w,
1241                None => return,
1242            };
1243            let document = match window.document() {
1244                Some(d) => d,
1245                None => return,
1246            };
1247
1248            // Create a closure that will be called when elements intersect
1249            // Use RefCell to allow mutation from within Fn closure
1250            use std::cell::RefCell;
1251            use std::rc::Rc;
1252
1253            let active_module_clone = Rc::new(RefCell::new(active_module.clone()));
1254            let active_module_for_closure = active_module_clone.clone();
1255
1256            let callback = Closure::<dyn Fn(js_sys::Array, web_sys::IntersectionObserver)>::new(
1257                move |entries: js_sys::Array, _observer: web_sys::IntersectionObserver| {
1258                    // Simple approach: when a module crosses the threshold line (enters from below),
1259                    // it becomes active. The threshold is set so modules activate when their top
1260                    // reaches ~100px from the top of the viewport.
1261                    for i in 0..entries.length() {
1262                        if let Ok(entry) = entries.get(i).dyn_into::<web_sys::IntersectionObserverEntry>() {
1263                            // Only activate when element is entering (crossing the threshold)
1264                            if entry.is_intersecting() {
1265                                let target = entry.target();
1266                                let id = target.id();
1267                                if !id.is_empty() {
1268                                    active_module_for_closure.borrow_mut().set(Some(id));
1269                                }
1270                            }
1271                        }
1272                    }
1273                },
1274            );
1275
1276            // Create IntersectionObserver options
1277            let mut options = web_sys::IntersectionObserverInit::new();
1278            // Root margin: top offset of -100px means the "viewport" starts 100px below the actual top
1279            // Bottom margin of -90% means only the top 10% of viewport triggers intersection
1280            // This creates a thin "tripwire" near the top of the screen
1281            options.root_margin("-100px 0px -90% 0px");
1282            // Single threshold at 0 - fires once when element crosses the line
1283            let thresholds = js_sys::Array::new();
1284            thresholds.push(&JsValue::from(0.0));
1285            options.threshold(&thresholds);
1286
1287            // Create the observer
1288            let observer = match web_sys::IntersectionObserver::new_with_options(
1289                callback.as_ref().unchecked_ref(),
1290                &options,
1291            ) {
1292                Ok(obs) => obs,
1293                Err(_) => return,
1294            };
1295
1296            // Observe all module cards
1297            for module_id in &module_ids_for_effect {
1298                if let Some(element) = document.get_element_by_id(module_id) {
1299                    observer.observe(&element);
1300                }
1301            }
1302
1303            // Keep callback alive
1304            callback.forget();
1305        });
1306    }
1307
1308    // Track first era for divider logic
1309    let mut is_first_era = true;
1310
1311    let breadcrumbs = vec![
1312        BreadcrumbItem { name: "Home", path: "/" },
1313        BreadcrumbItem { name: "Learn", path: "/learn" },
1314    ];
1315
1316    let schemas = vec![
1317        organization_schema(),
1318        course_schema(),
1319        breadcrumb_schema(&breadcrumbs),
1320    ];
1321
1322    rsx! {
1323        PageHead {
1324            title: seo_pages::LEARN.title,
1325            description: seo_pages::LEARN.description,
1326            canonical_path: seo_pages::LEARN.canonical_path,
1327        }
1328        style { "{LEARN_STYLE}" }
1329        JsonLdMultiple { schemas }
1330
1331        div { class: "learn-page",
1332            MainNav { active: ActivePage::Learn, subtitle: Some("Master formal reasoning") }
1333
1334            // Hero
1335            header { class: "learn-hero",
1336                div { class: "learn-hero-badge",
1337                    div { class: "dot" }
1338                    span { "Interactive Curriculum" }
1339                }
1340                h1 { "Learn Logic" }
1341                p {
1342                    "Master first-order logic through progressive challenges. Start with the basics and work your way up to advanced reasoning."
1343                }
1344            }
1345
1346            // Main layout
1347            div { class: "learn-layout",
1348                // Sidebar
1349                LearnSidebar {
1350                    modules: sidebar_modules,
1351                    active_module: active_module.read().clone(),
1352                    on_module_click: move |(_era_id, module_id): (String, String)| {
1353                        active_module.set(Some(module_id));
1354                    },
1355                }
1356
1357                // Content
1358                main { class: "learn-content",
1359                    for era in eras.iter() {
1360                        {
1361                            // Show divider for all eras except the first
1362                            let show_divider = !is_first_era;
1363                            is_first_era = false;
1364
1365                            rsx! {
1366                                // Era divider
1367                                if show_divider {
1368                                    div { class: "learn-era-divider",
1369                                        h2 { "{era.title}" }
1370                                    }
1371                                }
1372
1373                                // Era section
1374                                {
1375                                    let era_id_for_collapse = era.id.to_string();
1376                                    let is_collapsed = collapsed_eras.read().contains(&era_id_for_collapse);
1377                                    let header_class = if is_collapsed { "learn-era-header collapsible collapsed" } else { "learn-era-header collapsible" };
1378
1379                                    rsx! {
1380                                        section {
1381                                            class: "learn-era",
1382                                            div {
1383                                                class: "{header_class}",
1384                                                role: "button",
1385                                                tabindex: "0",
1386                                                aria_expanded: "{!is_collapsed}",
1387                                                aria_controls: "era-{era.id}-modules",
1388                                                onclick: {
1389                                                    let era_id = era_id_for_collapse.clone();
1390                                                    move |_| {
1391                                                        let mut set = collapsed_eras.write();
1392                                                        if set.contains(&era_id) {
1393                                                            set.remove(&era_id);
1394                                                        } else {
1395                                                            set.insert(era_id.clone());
1396                                                        }
1397                                                    }
1398                                                },
1399                                                onkeydown: {
1400                                                    let era_id = era_id_for_collapse.clone();
1401                                                    move |evt: KeyboardEvent| {
1402                                                        if evt.key() == Key::Enter || evt.key() == Key::Character(" ".to_string()) {
1403                                                            let mut set = collapsed_eras.write();
1404                                                            if set.contains(&era_id) {
1405                                                                set.remove(&era_id);
1406                                                            } else {
1407                                                                set.insert(era_id.clone());
1408                                                            }
1409                                                        }
1410                                                    }
1411                                                },
1412                                                div { class: "learn-era-header-content",
1413                                                    h2 { "{era.title}" }
1414                                                    p { "{era.description}" }
1415                                                }
1416                                                span {
1417                                                    class: if is_collapsed { "learn-era-chevron collapsed" } else { "learn-era-chevron" },
1418                                                    "▼"
1419                                                }
1420                                            }
1421
1422                                            if !is_collapsed {
1423                                                div {
1424                                                    id: "era-{era.id}-modules",
1425                                                    class: "learn-modules",
1426                                                    role: "region",
1427                                                    aria_labelledby: "era-{era.id}-header",
1428                                        for (idx, module) in era.modules.iter().enumerate() {
1429                                            {
1430                                                let era_id = era.id.to_string();
1431                                                let module_id = module.id.to_string();
1432                                                let module_number = idx + 1;
1433
1434                                                // All modules are now unlocked - content is freely accessible
1435                                                let _is_unlocked = true;
1436
1437                                                // Check if this module is expanded
1438                                                let is_expanded = expanded_module.read().as_ref()
1439                                                    .map(|(e, m)| e == era.id && m == module.id)
1440                                                    .unwrap_or(false);
1441
1442                                                let card_class = if is_expanded {
1443                                                    "learn-module-card expanded"
1444                                                } else {
1445                                                    "learn-module-card"
1446                                                };
1447
1448                                                rsx! {
1449                                                    div {
1450                                                        class: "{card_class}",
1451                                                        id: "{module.id}",
1452                                                        style: if is_expanded { "position: relative;" } else { "" },
1453
1454                                                        // Close button when expanded
1455                                                        if is_expanded {
1456                                                            {
1457                                                                let module_id_for_scroll = module_id.clone();
1458                                                                rsx! {
1459                                                                    button {
1460                                                                        class: "learn-module-close",
1461                                                                        onclick: move |_| {
1462                                                                            expanded_module.set(None);
1463                                                                            // Scroll to the module card after collapse
1464                                                                            #[cfg(target_arch = "wasm32")]
1465                                                                            {
1466                                                                                let id = module_id_for_scroll.clone();
1467                                                                                wasm_bindgen_futures::spawn_local(async move {
1468                                                                                    // Delay to let the DOM update after collapse
1469                                                                                    gloo_timers::future::TimeoutFuture::new(150).await;
1470                                                                                    if let Some(window) = web_sys::window() {
1471                                                                                        if let Some(document) = window.document() {
1472                                                                                            if let Some(element) = document.get_element_by_id(&id) {
1473                                                                                                // Get element position and scroll with offset for nav
1474                                                                                                let rect = element.get_bounding_client_rect();
1475                                                                                                let scroll_y = window.scroll_y().unwrap_or(0.0);
1476                                                                                                let target_y = scroll_y + rect.top() - 100.0; // 100px offset for nav
1477                                                                                                let _ = window.scroll_to_with_scroll_to_options(
1478                                                                                                    web_sys::ScrollToOptions::new()
1479                                                                                                        .top(target_y)
1480                                                                                                        .behavior(web_sys::ScrollBehavior::Smooth)
1481                                                                                                );
1482                                                                                            }
1483                                                                                        }
1484                                                                                    }
1485                                                                                });
1486                                                                            }
1487                                                                        },
1488                                                                        "×"
1489                                                                    }
1490                                                                }
1491                                                            }
1492                                                        }
1493
1494                                                        div { class: "learn-module-header",
1495                                                            div { class: "learn-module-info",
1496                                                                h3 { class: "learn-module-title",
1497                                                                    span { class: "learn-module-number", "{module_number}." }
1498                                                                    "{module.title}"
1499                                                                }
1500                                                                p { class: "learn-module-desc", "{module.description}" }
1501                                                            }
1502
1503                                                            div { class: "learn-module-meta",
1504                                                                span { class: "learn-exercise-count", "{module.exercise_count} exercises" }
1505                                                                div { class: "learn-difficulty",
1506                                                                    for i in 1..=5u8 {
1507                                                                        div {
1508                                                                            class: if i <= module.difficulty { "learn-difficulty-dot filled" } else { "learn-difficulty-dot" }
1509                                                                        }
1510                                                                    }
1511                                                                }
1512                                                            }
1513                                                        }
1514
1515                                                        // Show preview only when collapsed (all modules now accessible)
1516                                                        if !is_expanded {
1517                                                            if let Some(preview) = module.preview_code {
1518                                                                div { class: "learn-module-preview",
1519                                                                    div { class: "learn-preview-label", "Try an Example" }
1520                                                                    GuideCodeBlock {
1521                                                                        id: format!("preview-{}", module.id),
1522                                                                        label: "Example".to_string(),
1523                                                                        mode: ExampleMode::Logic,
1524                                                                        initial_code: preview.to_string(),
1525                                                                    }
1526                                                                }
1527                                                            }
1528
1529                                                            // Action buttons (all modules now accessible)
1530                                                            div { class: "learn-module-actions",
1531                                                                button {
1532                                                                    class: "learn-action-btn primary",
1533                                                                    onclick: {
1534                                                                        let era = era_id.clone();
1535                                                                        let module = module_id.clone();
1536                                                                        move |_| {
1537                                                                            expanded_module.set(Some((era.clone(), module.clone())));
1538                                                                        }
1539                                                                    },
1540                                                                    "Start Learning"
1541                                                                }
1542                                                            }
1543                                                        }
1544
1545                                                        // Expanded content - Interactive exercises
1546                                                        if is_expanded {
1547                                                            div { class: "learn-module-expanded-content",
1548                                                                InteractiveExercisePanel {
1549                                                                    era_id: era_id.clone(),
1550                                                                    module_id: module_id.clone(),
1551                                                                }
1552                                                            }
1553                                                        }
1554                                                    }
1555                                                }
1556                                            }
1557                                        }
1558                                                }
1559                                            }
1560                                        }
1561                                    }
1562                                }
1563                            }
1564                        }
1565                    }
1566                }
1567            }
1568
1569            // Floating vocab reference panel
1570            VocabReference {}
1571
1572            Footer {}
1573        }
1574    }
1575}
1576
1577/// What is currently revealed in the exercise - each section can be shown independently
1578#[derive(Debug, Clone, Copy, PartialEq, Default)]
1579struct RevealState {
1580    hint: bool,
1581    answer: bool,
1582    symbol_dictionary: bool,
1583}
1584
1585impl RevealState {
1586    fn reset(&mut self) {
1587        self.hint = false;
1588        self.answer = false;
1589        self.symbol_dictionary = false;
1590    }
1591}
1592
1593/// Practice mode state
1594#[derive(Debug, Clone, Copy, PartialEq, Default)]
1595enum PracticeMode {
1596    #[default]
1597    Practice,
1598    Test,
1599}
1600
1601/// Content view tab state - corresponds to the 4 tabs
1602#[derive(Debug, Clone, Copy, PartialEq, Default)]
1603enum ContentView {
1604    #[default]
1605    Lesson,
1606    Examples,
1607    Practice,
1608    Test,
1609}
1610
1611/// Interactive exercise panel with reveal buttons instead of tabs
1612#[component]
1613fn InteractiveExercisePanel(era_id: String, module_id: String) -> Element {
1614    rsx! {
1615        crate::ui::components::lexicon_gate::LexiconGate {
1616            InteractiveExercisePanelInner { era_id, module_id }
1617        }
1618    }
1619}
1620
1621#[component]
1622fn InteractiveExercisePanelInner(era_id: String, module_id: String) -> Element {
1623    use crate::content::{ContentBlock, Section};
1624
1625    let engine = ContentEngine::new();
1626    let generator = Generator::new();
1627
1628    // Content view state (Lesson vs Practice)
1629    let mut content_view = use_signal(ContentView::default);
1630
1631    // Exercise state
1632    let mut current_exercise_idx = use_signal(|| 0usize);
1633    let mut user_answer = use_signal(|| String::new());
1634    let mut reveal_state = use_signal(RevealState::default);
1635    let mut feedback = use_signal(|| None::<(bool, String)>);
1636    let mut score = use_signal(|| 0u32);
1637    let mut streak = use_signal(|| 0u32);
1638    let mut correct_count = use_signal(|| 0u32);
1639    let mut practice_mode = use_signal(PracticeMode::default);
1640    // Track wrong attempts per exercise - each wrong costs 5 XP, max 10 XP available per exercise
1641    // After 2 wrong attempts, no XP can be earned from that exercise
1642    let mut exercise_attempts = use_signal(|| std::collections::HashMap::<usize, u32>::new());
1643    // Track which exercises have been completed (earned XP) - prevents double XP
1644    let mut completed_exercises = use_signal(|| std::collections::HashSet::<usize>::new());
1645    // Track which exercises have had their answer revealed (forfeits XP)
1646    let mut answer_revealed_exercises = use_signal(|| std::collections::HashSet::<usize>::new());
1647
1648    // Priority queue for wrong answers - re-queue at position +3
1649    // When wrong, insert exercise index to be shown again after 3 more exercises
1650    let mut retry_queue = use_signal(|| std::collections::VecDeque::<usize>::new());
1651    // Count exercises since last retry to trigger queue pop after 3
1652    let mut exercises_since_retry = use_signal(|| 0usize);
1653
1654    // Test mode state
1655    let mut test_question = use_signal(|| 0usize);
1656    let mut test_answers = use_signal(|| Vec::<bool>::new());
1657    let mut test_complete = use_signal(|| false);
1658
1659    // Struggle detection state
1660    let mut struggle_detector = use_signal(StruggleDetector::default);
1661    let mut show_socratic_hint = use_signal(|| false);
1662
1663    // Lesson quiz state - tracks which quizzes have been answered and their result
1664    // Key: quiz question string, Value: (selected_index, is_correct)
1665    let mut quiz_answers = use_signal(|| std::collections::HashMap::<String, (usize, bool)>::new());
1666
1667    // Stable seed per exercise - only set once when component mounts or exercise changes
1668    // Use a signal to store the base seed so it doesn't change on re-renders
1669    let base_seed = use_signal(|| {
1670        #[cfg(target_arch = "wasm32")]
1671        { js_sys::Date::now() as u64 }
1672        #[cfg(not(target_arch = "wasm32"))]
1673        { 42u64 }
1674    });
1675
1676    // Generate challenge from exercise using stable seed
1677    let module_opt = engine.get_module(&era_id, &module_id);
1678    let current_challenge: Option<Challenge> = module_opt.as_ref().and_then(|module| {
1679        let idx = *current_exercise_idx.read();
1680        let seed = *base_seed.read();
1681        module.exercises.get(idx).and_then(|ex| {
1682            // Use exercise index as part of seed to get different sentences per exercise
1683            // but stable within the same exercise
1684            let mut rng = StdRng::seed_from_u64(seed.wrapping_add((idx * 1000) as u64));
1685            generator.generate(ex, &mut rng)
1686        })
1687    });
1688
1689    let total_exercises = module_opt.as_ref().map(|m| m.exercises.len()).unwrap_or(0);
1690    let current_idx = *current_exercise_idx.read();
1691    let progress_pct = if total_exercises > 0 {
1692        ((current_idx + 1) * 100) / total_exercises
1693    } else {
1694        0
1695    };
1696
1697    // Get the golden answer for validation
1698    let golden_answer = current_challenge.as_ref().and_then(|ch| {
1699        match &ch.answer {
1700            AnswerType::FreeForm { golden_logic } => Some(golden_logic.clone()),
1701            AnswerType::MultipleChoice { options, correct_index } => {
1702                options.get(*correct_index).cloned()
1703            }
1704            AnswerType::Ambiguity { readings } => readings.first().cloned(),
1705        }
1706    });
1707
1708    // Get hint from exercise
1709    let hint_text = current_challenge.as_ref().and_then(|ch| ch.hint.clone());
1710
1711    // Test mode constants
1712    let test_total = 10usize;
1713    let is_test_mode = *practice_mode.read() == PracticeMode::Test;
1714    let current_test_q = *test_question.read();
1715
1716    // Get sections for lesson content
1717    let sections: Vec<Section> = module_opt.as_ref()
1718        .map(|m| m.sections.clone())
1719        .unwrap_or_default();
1720    let has_sections = !sections.is_empty();
1721    let current_view = *content_view.read();
1722
1723    rsx! {
1724        div { class: "interactive-exercise-panel",
1725            // Content tabs (Lesson | Examples | Practice | Test)
1726            div { class: "content-tabs",
1727                button {
1728                    class: if current_view == ContentView::Lesson { "content-tab-btn active" } else { "content-tab-btn" },
1729                    onclick: move |_| content_view.set(ContentView::Lesson),
1730                    "Lesson"
1731                }
1732                button {
1733                    class: if current_view == ContentView::Examples { "content-tab-btn active" } else { "content-tab-btn" },
1734                    onclick: move |_| content_view.set(ContentView::Examples),
1735                    "Examples"
1736                }
1737                button {
1738                    class: if current_view == ContentView::Practice { "content-tab-btn practice active" } else { "content-tab-btn practice" },
1739                    onclick: move |_| {
1740                        content_view.set(ContentView::Practice);
1741                        practice_mode.set(PracticeMode::Practice);
1742                    },
1743                    "Practice"
1744                }
1745                button {
1746                    class: if current_view == ContentView::Test { "content-tab-btn test active" } else { "content-tab-btn test" },
1747                    onclick: move |_| {
1748                        content_view.set(ContentView::Test);
1749                        practice_mode.set(PracticeMode::Test);
1750                        test_question.set(0);
1751                        test_answers.set(Vec::new());
1752                        test_complete.set(false);
1753                        user_answer.set(String::new());
1754                        feedback.set(None);
1755                    },
1756                    "Test"
1757                }
1758            }
1759
1760            // Lesson content view
1761            if current_view == ContentView::Lesson {
1762                div { class: "tab-panel-lesson",
1763                    if has_sections {
1764                        for section in sections.iter() {
1765                            div { class: "lesson-section",
1766                                h3 { class: "lesson-section-title",
1767                                    span { class: "lesson-section-number", "{section.id}" }
1768                                    "{section.title}"
1769                                }
1770
1771                                // Render content blocks
1772                                for block in section.content.iter() {
1773                                    match block {
1774                                        ContentBlock::Paragraph { text } => rsx! {
1775                                            p { class: "lesson-paragraph",
1776                                                dangerous_inner_html: text.replace("**", "<strong>").replace("**", "</strong>").replace("*", "<em>").replace("*", "</em>")
1777                                            }
1778                                        },
1779                                        ContentBlock::Definition { term, definition } => rsx! {
1780                                            div { class: "lesson-definition",
1781                                                div { class: "lesson-definition-term", "{term}" }
1782                                                div { class: "lesson-definition-text", "{definition}" }
1783                                            }
1784                                        },
1785                                        ContentBlock::Example { title, premises, conclusion, note } => rsx! {
1786                                            div { class: "lesson-example",
1787                                                div { class: "lesson-example-title", "{title}" }
1788                                                for premise in premises.iter() {
1789                                                    div { class: "lesson-example-premise", "• {premise}" }
1790                                                }
1791                                                if let Some(concl) = conclusion {
1792                                                    div { class: "lesson-example-conclusion", "{concl}" }
1793                                                }
1794                                                if let Some(n) = note {
1795                                                    div { class: "lesson-example-note", "{n}" }
1796                                                }
1797                                            }
1798                                        },
1799                                        ContentBlock::Symbols { title, symbols } => rsx! {
1800                                            div { class: "lesson-symbols",
1801                                                div { class: "lesson-symbols-title", "{title}" }
1802                                                div { class: "lesson-symbols-grid",
1803                                                    for sym in symbols.iter() {
1804                                                        div { class: "lesson-symbol-item",
1805                                                            span { class: "lesson-symbol-glyph", "{sym.symbol}" }
1806                                                            div { class: "lesson-symbol-info",
1807                                                                div { class: "lesson-symbol-name", "{sym.name}" }
1808                                                                div { class: "lesson-symbol-meaning", "{sym.meaning}" }
1809                                                                if let Some(example) = &sym.example {
1810                                                                    div { class: "lesson-symbol-example", "Example: {example}" }
1811                                                                }
1812                                                            }
1813                                                        }
1814                                                    }
1815                                                }
1816                                            }
1817                                        },
1818                                        ContentBlock::Quiz { question, options, correct, explanation } => {
1819                                            let q_key = question.clone();
1820                                            let answered = quiz_answers.read().get(&q_key).cloned();
1821                                            let correct_idx = *correct;
1822                                            rsx! {
1823                                                div { class: "lesson-quiz",
1824                                                    div { class: "lesson-quiz-question", "{question}" }
1825                                                    div { class: "lesson-quiz-options",
1826                                                        for (i, opt) in options.iter().enumerate() {
1827                                                            {
1828                                                                let q_key_click = q_key.clone();
1829                                                                let opt_class = match &answered {
1830                                                                    Some((selected, _)) if *selected == i && i == correct_idx => "lesson-quiz-option correct",
1831                                                                    Some((selected, _)) if *selected == i => "lesson-quiz-option incorrect",
1832                                                                    Some(_) if i == correct_idx => "lesson-quiz-option correct",
1833                                                                    Some(_) => "lesson-quiz-option answered",
1834                                                                    None => "lesson-quiz-option",
1835                                                                };
1836                                                                rsx! {
1837                                                                    button {
1838                                                                        class: "{opt_class}",
1839                                                                        disabled: answered.is_some(),
1840                                                                        onclick: move |_| {
1841                                                                            let is_correct = i == correct_idx;
1842                                                                            quiz_answers.write().insert(q_key_click.clone(), (i, is_correct));
1843                                                                        },
1844                                                                        "{opt}"
1845                                                                    }
1846                                                                }
1847                                                            }
1848                                                        }
1849                                                    }
1850                                                    if answered.is_some() {
1851                                                        if let Some(expl) = explanation {
1852                                                            div { class: "lesson-quiz-explanation visible", "{expl}" }
1853                                                        }
1854                                                    }
1855                                                }
1856                                            }
1857                                        },
1858                                    }
1859                                }
1860
1861                            }
1862                        }
1863
1864                        // Start practicing button
1865                        div { class: "learn-module-actions", style: "justify-content: center; margin-top: var(--spacing-xl);",
1866                            button {
1867                                class: "learn-action-btn primary",
1868                                onclick: move |_| content_view.set(ContentView::Examples),
1869                                "Try Examples →"
1870                            }
1871                        }
1872                    } else {
1873                        p { style: "color: var(--text-tertiary); text-align: center; padding: var(--spacing-xl);",
1874                            "Lesson content coming soon. Click Examples to see interactive demos."
1875                        }
1876                    }
1877                }
1878            } else if current_view == ContentView::Examples {
1879                // Examples view - Interactive code execution
1880                div { class: "tab-panel-examples",
1881                    div { class: "examples-intro",
1882                        h3 { "Interactive Examples" }
1883                        p { "Try translating sentences and see how logic notation works. The symbol dictionary will explain each symbol used." }
1884                    }
1885
1886                    // Example sentences to try
1887                    div { class: "examples-list",
1888                        {
1889                            let examples: Vec<(&str, &str)> = vec![
1890                                ("ex1", "All cats are mammals."),
1891                                ("ex2", "Socrates is mortal."),
1892                                ("ex3", "If it rains, the ground is wet."),
1893                                ("ex4", "Some birds can fly."),
1894                                ("ex5", "No reptiles are mammals."),
1895                            ];
1896
1897                            rsx! {
1898                                for (id, sentence) in examples {
1899                                    div { class: "example-card",
1900                                        key: "{id}",
1901                                        div { class: "example-sentence", "{sentence}" }
1902                                        GuideCodeBlock {
1903                                            id: id.to_string(),
1904                                            label: "Try it".to_string(),
1905                                            mode: ExampleMode::Logic,
1906                                            initial_code: sentence.to_string(),
1907                                        }
1908                                    }
1909                                }
1910                            }
1911                        }
1912                    }
1913
1914                    // Navigation to Practice
1915                    div { class: "learn-module-actions", style: "justify-content: center; margin-top: var(--spacing-xl);",
1916                        button {
1917                            class: "learn-action-btn primary",
1918                            onclick: move |_| {
1919                                content_view.set(ContentView::Practice);
1920                                practice_mode.set(PracticeMode::Practice);
1921                            },
1922                            "Start Practice →"
1923                        }
1924                    }
1925                }
1926            } else {
1927                // Practice/Test view
1928                // Stats header
1929                div { class: "mode-stats",
1930                    div { class: "mode-stat",
1931                        span { class: "mode-stat-value", "{score}" }
1932                        span { class: "mode-stat-label", " XP" }
1933                    }
1934                    if *streak.read() > 0 {
1935                        {
1936                            let s = *streak.read();
1937                            let mult = match s {
1938                                0 => "1x",
1939                                1 => "1.25x",
1940                                2 => "1.5x",
1941                                3 => "1.75x",
1942                                _ => "2x",
1943                            };
1944                            rsx! {
1945                                div { class: "mode-stat combo",
1946                                    span { class: "mode-stat-value streak", "{s}" }
1947                                    span { class: "mode-stat-label", " streak " }
1948                                    span { class: "combo-multiplier", "({mult})" }
1949                                }
1950                            }
1951                        }
1952                    }
1953                    div { class: "mode-stat",
1954                        span { class: "mode-stat-value", "{correct_count}" }
1955                        span { class: "mode-stat-label", " correct" }
1956                    }
1957                }
1958
1959            // Test mode header
1960            if is_test_mode {
1961                if *test_complete.read() {
1962                    // Test results
1963                    div { class: "test-results",
1964                        style: "text-align: center; padding: var(--spacing-xxl);",
1965                        h3 { style: "margin-bottom: var(--spacing-lg); font-size: var(--font-heading-lg);",
1966                            "Test Complete!"
1967                        }
1968                        {
1969                            let answers = test_answers.read();
1970                            let correct = answers.iter().filter(|&&c| c).count();
1971                            let pct = (correct * 100) / test_total;
1972                            let grade = if pct >= 90 { "A" } else if pct >= 80 { "B" } else if pct >= 70 { "C" } else if pct >= 60 { "D" } else { "F" };
1973                            rsx! {
1974                                div { style: "font-size: var(--font-display-md); font-weight: 700; color: var(--color-accent-blue);",
1975                                    "{correct}/{test_total}"
1976                                }
1977                                div { style: "font-size: var(--font-heading-lg); color: var(--text-secondary); margin: var(--spacing-md) 0;",
1978                                    "Grade: {grade} ({pct}%)"
1979                                }
1980                            }
1981                        }
1982                        div { class: "learn-module-actions", style: "justify-content: center; margin-top: var(--spacing-xl);",
1983                            button {
1984                                class: "learn-action-btn secondary",
1985                                onclick: move |_| {
1986                                    practice_mode.set(PracticeMode::Practice);
1987                                    test_complete.set(false);
1988                                },
1989                                "Back to Practice"
1990                            }
1991                            button {
1992                                class: "learn-action-btn primary",
1993                                onclick: move |_| {
1994                                    test_question.set(0);
1995                                    test_answers.set(Vec::new());
1996                                    test_complete.set(false);
1997                                    user_answer.set(String::new());
1998                                    feedback.set(None);
1999                                },
2000                                "Retake Test"
2001                            }
2002                        }
2003                    }
2004                } else {
2005                    // Test mode progress
2006                    div { class: "exercise-progress",
2007                        div { class: "exercise-mode-badge test", "TEST MODE" }
2008                        span { "Question {current_test_q + 1} of {test_total}" }
2009                        div { class: "progress-bar",
2010                            div {
2011                                class: "progress-fill",
2012                                style: "width: {((current_test_q + 1) * 100) / test_total}%",
2013                            }
2014                        }
2015                    }
2016                }
2017            } else {
2018                // Practice mode progress
2019                div { class: "exercise-progress",
2020                    span { "Exercise {current_idx + 1} of {total_exercises}" }
2021                    div { class: "progress-bar",
2022                        div {
2023                            class: "progress-fill",
2024                            style: "width: {progress_pct}%",
2025                        }
2026                    }
2027                    span { class: "practice-score", "+{score} XP" }
2028                    if *correct_count.read() > 0 {
2029                        span { style: "color: var(--color-success); font-size: var(--font-caption-md);",
2030                            " ({correct_count} correct)"
2031                        }
2032                    }
2033                }
2034            }
2035
2036            // Don't show exercise card when test is complete
2037            if !(is_test_mode && *test_complete.read()) {
2038            if let Some(challenge) = current_challenge.as_ref() {
2039                div { class: "exercise-card",
2040                    div { class: "exercise-sentence", "{challenge.sentence}" }
2041
2042                    // Answer input based on exercise type
2043                    match &challenge.answer {
2044                        AnswerType::FreeForm { .. } => rsx! {
2045                            div { class: "exercise-input-row",
2046                                input {
2047                                    class: match feedback.read().as_ref() {
2048                                        Some((true, _)) => "exercise-input correct",
2049                                        Some((false, _)) => "exercise-input incorrect",
2050                                        None => "exercise-input",
2051                                    },
2052                                    r#type: "text",
2053                                    placeholder: "Enter your logic translation... (Enter to submit)",
2054                                    value: "{user_answer}",
2055                                    oninput: {
2056                                        move |e: Event<FormData>| {
2057                                            user_answer.set(e.value());
2058                                            // Record activity to reset inactivity timer
2059                                            struggle_detector.write().record_activity();
2060                                        }
2061                                    },
2062                                    onkeydown: {
2063                                        let golden = golden_answer.clone();
2064                                        move |e: Event<KeyboardData>| {
2065                                            if e.key() == Key::Enter {
2066                                                e.prevent_default();
2067                                                // Trigger submit logic - same as Check button
2068                                                let answer = user_answer.read().clone();
2069                                                if !answer.is_empty() {
2070                                                    if let Some(ref expected) = golden {
2071                                                        let result = check_answer(&answer, expected);
2072                                                        if result.correct {
2073                                                            // Handle correct answer
2074                                                            let answer_was_revealed = answer_revealed_exercises.read().contains(&current_idx);
2075                                                            let already_completed = if is_test_mode { false } else { completed_exercises.read().contains(&current_idx) };
2076
2077                                                            if answer_was_revealed {
2078                                                                let cc = *correct_count.read();
2079                                                                correct_count.set(cc + 1);
2080                                                                completed_exercises.write().insert(current_idx);
2081                                                                feedback.set(Some((true, "Correct! (no XP - answer was revealed)".to_string())));
2082                                                            } else if !already_completed {
2083                                                                let wrong_count = *exercise_attempts.read().get(&current_idx).unwrap_or(&0);
2084                                                                let base_xp = 10u32.saturating_sub(wrong_count * 5);
2085                                                                if base_xp > 0 {
2086                                                                    let cs = *streak.read();
2087                                                                    let sc = *score.read();
2088                                                                    let cc = *correct_count.read();
2089                                                                    let multiplier = match cs { 0 => 1.0, 1 => 1.25, 2 => 1.5, 3 => 1.75, _ => 2.0 };
2090                                                                    let xp = ((base_xp as f64) * multiplier).round() as u32;
2091                                                                    score.set(sc + xp);
2092                                                                    streak.set(cs + 1);
2093                                                                    correct_count.set(cc + 1);
2094                                                                    completed_exercises.write().insert(current_idx);
2095                                                                    let msg = if multiplier > 1.0 { format!("Correct! +{} XP ({}x combo)", xp, multiplier) } else { format!("Correct! +{} XP", xp) };
2096                                                                    feedback.set(Some((true, msg)));
2097                                                                } else {
2098                                                                    let cc = *correct_count.read();
2099                                                                    correct_count.set(cc + 1);
2100                                                                    completed_exercises.write().insert(current_idx);
2101                                                                    feedback.set(Some((true, "Correct! (no XP - too many attempts)".to_string())));
2102                                                                }
2103                                                            } else {
2104                                                                feedback.set(Some((true, "Correct! (already completed)".to_string())));
2105                                                            }
2106                                                            struggle_detector.write().record_correct_attempt();
2107                                                            show_socratic_hint.set(false);
2108                                                        } else {
2109                                                            // Wrong answer
2110                                                            let attempts = exercise_attempts.read().get(&current_idx).copied().unwrap_or(0);
2111                                                            exercise_attempts.write().insert(current_idx, attempts + 1);
2112                                                            let remaining = 10u32.saturating_sub((attempts + 1) * 5);
2113                                                            let penalty_msg = if remaining > 0 { format!(" (-5 XP, {} remaining)", remaining) } else { " (no XP remaining)".to_string() };
2114                                                            feedback.set(Some((false, format!("{}{}", result.feedback, penalty_msg))));
2115                                                            struggle_detector.write().record_wrong_attempt();
2116                                                            show_socratic_hint.set(true);
2117                                                            streak.set(0);
2118                                                            if !is_test_mode {
2119                                                                let mut queue = retry_queue.write();
2120                                                                if !queue.contains(&current_idx) { queue.push_back(current_idx); }
2121                                                            }
2122                                                        }
2123                                                    }
2124                                                }
2125                                            }
2126                                        }
2127                                    },
2128                                }
2129                                button {
2130                                    class: "exercise-submit-btn",
2131                                    onclick: {
2132                                        let golden = golden_answer.clone();
2133                                        move |_| {
2134                                            let answer = user_answer.read().clone();
2135                                            if !answer.is_empty() {
2136                                                if let Some(ref expected) = golden {
2137                                                    // Use the real grader
2138                                                    let result = check_answer(&answer, expected);
2139                                                    if result.correct {
2140                                                        // Check if answer was revealed (forfeits XP)
2141                                                        let answer_was_revealed = answer_revealed_exercises.read().contains(&current_idx);
2142
2143                                                        // Only check completed_exercises in practice mode, not test mode
2144                                                        let already_completed = if is_test_mode {
2145                                                            false // Test mode always gives fresh XP
2146                                                        } else {
2147                                                            completed_exercises.read().contains(&current_idx)
2148                                                        };
2149
2150                                                        if answer_was_revealed {
2151                                                            // Answer was revealed - no XP
2152                                                            let current_correct = *correct_count.read();
2153                                                            correct_count.set(current_correct + 1);
2154                                                            completed_exercises.write().insert(current_idx);
2155                                                            feedback.set(Some((true, "Correct! (no XP - answer was revealed)".to_string())));
2156                                                        } else if !already_completed {
2157                                                            // Calculate XP based on wrong attempts (each wrong costs 5 XP)
2158                                                            let wrong_count = *exercise_attempts.read().get(&current_idx).unwrap_or(&0);
2159                                                            let base_xp = 10u32.saturating_sub(wrong_count * 5);
2160
2161                                                            if base_xp > 0 {
2162                                                                let current_streak = *streak.read();
2163                                                                let current_score = *score.read();
2164                                                                let current_correct = *correct_count.read();
2165
2166                                                                // Combo multiplier: 1.0x, 1.25x, 1.5x, 1.75x, 2.0x
2167                                                                let multiplier = match current_streak {
2168                                                                    0 => 1.0,
2169                                                                    1 => 1.25,
2170                                                                    2 => 1.5,
2171                                                                    3 => 1.75,
2172                                                                    _ => 2.0, // Max 2x
2173                                                                };
2174                                                                let xp = ((base_xp as f64) * multiplier).round() as u32;
2175
2176                                                                score.set(current_score + xp);
2177                                                                streak.set(current_streak + 1);
2178                                                                correct_count.set(current_correct + 1);
2179                                                                completed_exercises.write().insert(current_idx);
2180
2181                                                                let msg = if multiplier > 1.0 {
2182                                                                    format!("Correct! +{} XP ({}x combo)", xp, multiplier)
2183                                                                } else {
2184                                                                    format!("Correct! +{} XP", xp)
2185                                                                };
2186                                                                feedback.set(Some((true, msg)));
2187                                                            } else {
2188                                                                // Too many wrong attempts - no XP but still mark complete
2189                                                                let current_correct = *correct_count.read();
2190                                                                correct_count.set(current_correct + 1);
2191                                                                completed_exercises.write().insert(current_idx);
2192                                                                feedback.set(Some((true, "Correct! (no XP - too many attempts)".to_string())));
2193                                                            }
2194                                                        } else {
2195                                                            // Already earned XP for this exercise
2196                                                            feedback.set(Some((true, "Correct! (already completed)".to_string())));
2197                                                        }
2198                                                        struggle_detector.write().record_correct_attempt();
2199                                                        show_socratic_hint.set(false);
2200                                                    } else {
2201                                                        // Wrong answer - record attempt and show feedback
2202                                                        let attempts = exercise_attempts.read().get(&current_idx).copied().unwrap_or(0);
2203                                                        exercise_attempts.write().insert(current_idx, attempts + 1);
2204
2205                                                        let remaining = 10u32.saturating_sub((attempts + 1) * 5);
2206                                                        let penalty_msg = if remaining > 0 {
2207                                                            format!(" (-5 XP, {} remaining)", remaining)
2208                                                        } else {
2209                                                            " (no XP remaining)".to_string()
2210                                                        };
2211
2212                                                        feedback.set(Some((false, format!("{}{}", result.feedback, penalty_msg))));
2213                                                        struggle_detector.write().record_wrong_attempt();
2214                                                        show_socratic_hint.set(true);
2215                                                        streak.set(0);
2216
2217                                                        // In practice mode, add wrong exercise to retry queue (at +3 position)
2218                                                        if !is_test_mode {
2219                                                            let mut queue = retry_queue.write();
2220                                                            // Only add if not already in queue
2221                                                            if !queue.contains(&current_idx) {
2222                                                                queue.push_back(current_idx);
2223                                                            }
2224                                                        }
2225                                                    }
2226                                                }
2227                                            }
2228                                        }
2229                                    },
2230                                    "Check"
2231                                }
2232                            }
2233                        },
2234                        AnswerType::MultipleChoice { options, correct_index } => rsx! {
2235                            div { class: "multiple-choice-options",
2236                                for (idx, option) in options.iter().enumerate() {
2237                                    {
2238                                        let is_selected = user_answer.read().as_str() == option;
2239                                        let is_correct_option = idx == *correct_index;
2240                                        let option_clone = option.clone();
2241                                        let show_result = feedback.read().is_some();
2242
2243                                        let btn_class = if show_result && is_selected {
2244                                            if is_correct_option { "reveal-btn active correct" } else { "reveal-btn active incorrect" }
2245                                        } else if is_selected {
2246                                            "reveal-btn active"
2247                                        } else {
2248                                            "reveal-btn"
2249                                        };
2250
2251                                        rsx! {
2252                                            button {
2253                                                class: "{btn_class}",
2254                                                onclick: {
2255                                                    let opt = option_clone.clone();
2256                                                    let correct = is_correct_option;
2257                                                    move |_| {
2258                                                        user_answer.set(opt.clone());
2259                                                        if correct {
2260                                                            // Check if answer was revealed (forfeits XP)
2261                                                            let answer_was_revealed = answer_revealed_exercises.read().contains(&current_idx);
2262
2263                                                            // Only check completed_exercises in practice mode, not test mode
2264                                                            let already_completed = if is_test_mode {
2265                                                                false // Test mode always gives fresh XP
2266                                                            } else {
2267                                                                completed_exercises.read().contains(&current_idx)
2268                                                            };
2269
2270                                                            if answer_was_revealed {
2271                                                                // Answer was revealed - no XP
2272                                                                let current_correct = *correct_count.read();
2273                                                                correct_count.set(current_correct + 1);
2274                                                                completed_exercises.write().insert(current_idx);
2275                                                                feedback.set(Some((true, "Correct! (no XP - answer was revealed)".to_string())));
2276                                                            } else if !already_completed {
2277                                                                // Calculate XP based on wrong attempts (each wrong costs 5 XP)
2278                                                                let wrong_count = *exercise_attempts.read().get(&current_idx).unwrap_or(&0);
2279                                                                let base_xp = 10u32.saturating_sub(wrong_count * 5);
2280
2281                                                                if base_xp > 0 {
2282                                                                    let current_streak = *streak.read();
2283                                                                    let current_score = *score.read();
2284                                                                    let current_correct = *correct_count.read();
2285
2286                                                                    // Combo multiplier: 1.0x, 1.25x, 1.5x, 1.75x, 2.0x
2287                                                                    let multiplier = match current_streak {
2288                                                                        0 => 1.0,
2289                                                                        1 => 1.25,
2290                                                                        2 => 1.5,
2291                                                                        3 => 1.75,
2292                                                                        _ => 2.0, // Max 2x
2293                                                                    };
2294                                                                    let xp = ((base_xp as f64) * multiplier).round() as u32;
2295
2296                                                                    score.set(current_score + xp);
2297                                                                    streak.set(current_streak + 1);
2298                                                                    correct_count.set(current_correct + 1);
2299                                                                    completed_exercises.write().insert(current_idx);
2300
2301                                                                    let msg = if multiplier > 1.0 {
2302                                                                        format!("Correct! +{} XP ({}x combo)", xp, multiplier)
2303                                                                    } else {
2304                                                                        format!("Correct! +{} XP", xp)
2305                                                                    };
2306                                                                    feedback.set(Some((true, msg)));
2307                                                                } else {
2308                                                                    // Too many wrong attempts - no XP but still mark complete
2309                                                                    let current_correct = *correct_count.read();
2310                                                                    correct_count.set(current_correct + 1);
2311                                                                    completed_exercises.write().insert(current_idx);
2312                                                                    feedback.set(Some((true, "Correct! (no XP - too many attempts)".to_string())));
2313                                                                }
2314                                                            } else {
2315                                                                feedback.set(Some((true, "Correct! (already completed)".to_string())));
2316                                                            }
2317                                                            struggle_detector.write().record_correct_attempt();
2318                                                            show_socratic_hint.set(false);
2319                                                        } else {
2320                                                            // Wrong answer - record attempt and show feedback
2321                                                            let attempts = exercise_attempts.read().get(&current_idx).copied().unwrap_or(0);
2322                                                            exercise_attempts.write().insert(current_idx, attempts + 1);
2323
2324                                                            let remaining = 10u32.saturating_sub((attempts + 1) * 5);
2325                                                            let penalty_msg = if remaining > 0 {
2326                                                                format!(" (-5 XP, {} remaining)", remaining)
2327                                                            } else {
2328                                                                " (no XP remaining)".to_string()
2329                                                            };
2330
2331                                                            feedback.set(Some((false, format!("Not quite.{}", penalty_msg))));
2332                                                            struggle_detector.write().record_wrong_attempt();
2333                                                            show_socratic_hint.set(true);
2334                                                            streak.set(0);
2335
2336                                                            // In practice mode, add wrong exercise to retry queue
2337                                                            if !is_test_mode {
2338                                                                let mut queue = retry_queue.write();
2339                                                                if !queue.contains(&current_idx) {
2340                                                                    queue.push_back(current_idx);
2341                                                                }
2342                                                            }
2343                                                        }
2344                                                    }
2345                                                },
2346                                                "{option}"
2347                                            }
2348                                        }
2349                                    }
2350                                }
2351                            }
2352                        },
2353                        AnswerType::Ambiguity { readings } => rsx! {
2354                            div { class: "ambiguity-readings",
2355                                p { class: "exercise-prompt", "This sentence has {readings.len()} possible interpretations:" }
2356                                for (i, reading) in readings.iter().enumerate() {
2357                                    div { class: "revealed-logic",
2358                                        span { class: "revealed-label", "Reading {i + 1}" }
2359                                        "{reading}"
2360                                    }
2361                                }
2362                            }
2363                        },
2364                    }
2365
2366                    // Show feedback
2367                    if let Some((is_correct, msg)) = feedback.read().as_ref() {
2368                        div {
2369                            class: if *is_correct { "exercise-feedback correct" } else { "exercise-feedback incorrect" },
2370                            "{msg}"
2371                        }
2372                    }
2373
2374                    // Socratic hint (triggered by wrong answer or inactivity) - NOT in test mode
2375                    if !is_test_mode && *show_socratic_hint.read() && hint_text.is_some() {
2376                        div { class: "socratic-hint-box",
2377                            div { class: "hint-header",
2378                                Icon { variant: IconVariant::Owl, size: IconSize::Medium, color: "#a78bfa" }
2379                                " Socrates says..."
2380                            }
2381                            div { class: "hint-text",
2382                                if let Some(reason) = struggle_detector.read().reason() {
2383                                    "{reason.message()} "
2384                                }
2385                                if let Some(hint) = hint_text.as_ref() {
2386                                    "{hint}"
2387                                }
2388                            }
2389                        }
2390                    }
2391
2392                    // Interactive reveal buttons - NOT in test mode
2393                    if !is_test_mode {
2394                    div { class: "reveal-section",
2395                        div { class: "reveal-buttons",
2396                            // Show Hint button - toggles independently
2397                            button {
2398                                class: if reveal_state.read().hint { "reveal-btn active" } else { "reveal-btn" },
2399                                onclick: move |_| {
2400                                    let current = reveal_state.read().hint;
2401                                    reveal_state.write().hint = !current;
2402                                },
2403                                Icon { variant: IconVariant::Lightning, size: IconSize::Small }
2404                                " Show Hint"
2405                            }
2406
2407                            // Show Answer button - toggles independently, forfeits XP when revealed
2408                            button {
2409                                class: if reveal_state.read().answer { "reveal-btn active" } else { "reveal-btn" },
2410                                onclick: move |_| {
2411                                    let current = reveal_state.read().answer;
2412                                    if !current {
2413                                        // Revealing answer for first time - forfeit XP for this exercise
2414                                        answer_revealed_exercises.write().insert(current_idx);
2415                                    }
2416                                    reveal_state.write().answer = !current;
2417                                },
2418                                "✓ Show Answer (No XP)"
2419                            }
2420
2421                            // Symbol Dictionary button (only for FreeForm/Ambiguity) - toggles independently
2422                            if matches!(&challenge.answer, AnswerType::FreeForm { .. } | AnswerType::Ambiguity { .. }) {
2423                                button {
2424                                    class: if reveal_state.read().symbol_dictionary { "reveal-btn active" } else { "reveal-btn" },
2425                                    onclick: move |_| {
2426                                        let current = reveal_state.read().symbol_dictionary;
2427                                        reveal_state.write().symbol_dictionary = !current;
2428                                    },
2429                                    Icon { variant: IconVariant::Book, size: IconSize::Small }
2430                                    " Symbol Dictionary"
2431                                }
2432                            }
2433                        }
2434
2435                        // Stacked revealed content - each section shows independently
2436                        if reveal_state.read().hint {
2437                            div { class: "revealed-content",
2438                                div { class: "revealed-label", "Hint" }
2439                                if let Some(hint) = hint_text.as_ref() {
2440                                    p { "{hint}" }
2441                                } else {
2442                                    p { "No hint available for this exercise." }
2443                                }
2444                            }
2445                        }
2446
2447                        if reveal_state.read().answer {
2448                            div { class: "revealed-content",
2449                                div { class: "revealed-label", "Correct Answer" }
2450                                if let Some(answer) = golden_answer.as_ref() {
2451                                    div { class: "revealed-logic", "{answer}" }
2452                                    // Show explanation if available
2453                                    if let Some(explanation) = challenge.explanation.as_ref() {
2454                                        p { style: "margin-top: 12px; color: var(--text-secondary);", "{explanation}" }
2455                                    }
2456                                }
2457                            }
2458                        }
2459
2460                        if reveal_state.read().symbol_dictionary {
2461                            div { class: "revealed-content",
2462                                if let Some(answer) = golden_answer.as_ref() {
2463                                    SymbolDictionary { logic: answer.clone() }
2464                                } else {
2465                                    p { "No logic output to analyze." }
2466                                }
2467                            }
2468                        }
2469                    }
2470                    } // end if !is_test_mode
2471
2472                    // Action buttons
2473                    div { class: "learn-module-actions", style: "margin-top: 24px;",
2474                        if is_test_mode {
2475                            // Test mode: Submit answer and move to next question
2476                            button {
2477                                class: "learn-action-btn primary",
2478                                onclick: {
2479                                    let golden = golden_answer.clone();
2480                                    move |_| {
2481                                        let answer = user_answer.read().clone();
2482                                        let is_correct = if let Some(ref expected) = golden {
2483                                            if !answer.is_empty() {
2484                                                check_answer(&answer, expected).correct
2485                                            } else {
2486                                                false
2487                                            }
2488                                        } else {
2489                                            false
2490                                        };
2491
2492                                        // Record answer
2493                                        test_answers.write().push(is_correct);
2494
2495                                        // Move to next question or complete
2496                                        let next_q = current_test_q + 1;
2497                                        if next_q >= test_total {
2498                                            test_complete.set(true);
2499                                        } else {
2500                                            test_question.set(next_q);
2501                                            // Also advance the exercise index for variety
2502                                            let next_idx = (current_idx + 1) % total_exercises;
2503                                            current_exercise_idx.set(next_idx);
2504                                        }
2505                                        user_answer.set(String::new());
2506                                        feedback.set(None);
2507                                    }
2508                                },
2509                                "Submit Answer →"
2510                            }
2511                            button {
2512                                class: "learn-action-btn secondary",
2513                                onclick: move |_| {
2514                                    practice_mode.set(PracticeMode::Practice);
2515                                },
2516                                "Exit Test"
2517                            }
2518                        } else {
2519                        // Practice mode buttons
2520                        button {
2521                            class: "learn-action-btn secondary",
2522                            onclick: {
2523                                move |_| {
2524                                    // Simple linear progression - skip to next
2525                                    let next = current_idx + 1;
2526                                    if next < total_exercises {
2527                                        current_exercise_idx.set(next);
2528                                    } else {
2529                                        current_exercise_idx.set(0); // Loop back to start
2530                                    }
2531                                    // Reset state
2532                                    user_answer.set(String::new());
2533                                    feedback.set(None);
2534                                    reveal_state.write().reset();
2535                                    struggle_detector.write().reset();
2536                                    show_socratic_hint.set(false);
2537                                }
2538                            },
2539                            "Skip →"
2540                        }
2541
2542                        if feedback.read().as_ref().map(|(c, _)| *c).unwrap_or(false) {
2543                            button {
2544                                class: "learn-action-btn primary",
2545                                onclick: {
2546                                    move |_| {
2547                                        // Simple linear progression after correct answer
2548                                        let next = current_idx + 1;
2549                                        if next < total_exercises {
2550                                            current_exercise_idx.set(next);
2551                                        } else {
2552                                            current_exercise_idx.set(0); // Loop back to start
2553                                        }
2554                                        user_answer.set(String::new());
2555                                        feedback.set(None);
2556                                        reveal_state.write().reset();
2557                                        struggle_detector.write().reset();
2558                                        show_socratic_hint.set(false);
2559                                    }
2560                                },
2561                                "Next Exercise →"
2562                            }
2563                        }
2564
2565                        // Show "Take Test" button after 5 correct answers
2566                        if *correct_count.read() >= 5 {
2567                            button {
2568                                class: "learn-action-btn test-ready",
2569                                style: "background: linear-gradient(135deg, #fbbf24, #f59e0b); margin-left: auto;",
2570                                onclick: move |_| {
2571                                    practice_mode.set(PracticeMode::Test);
2572                                    test_question.set(0);
2573                                    test_answers.set(Vec::new());
2574                                    test_complete.set(false);
2575                                    user_answer.set(String::new());
2576                                    feedback.set(None);
2577                                    reveal_state.write().reset();
2578                                },
2579                                Icon { variant: IconVariant::Target, size: IconSize::Small }
2580                                " Take Test"
2581                            }
2582                        }
2583                        } // end else (practice mode)
2584                    }
2585                }
2586            } else {
2587                div { style: "text-align: center; padding: var(--spacing-xl); color: var(--text-secondary);",
2588                    p { "No exercises available for this module yet." }
2589                    p { style: "font-size: var(--font-caption-md); margin-top: var(--spacing-md);",
2590                        "Total loaded: {total_exercises}"
2591                    }
2592                }
2593            }
2594            } // end if !(is_test_mode && test_complete)
2595            } // end else (Practice view)
2596        }
2597    }
2598}