1use 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
996struct 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
1006struct 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 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 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 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 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
1195type ExpandedModuleKey = Option<(String, String)>;
1197
1198#[component(lazy)]
1199pub fn Learn() -> Element {
1200 let mut active_module = use_signal(|| None::<String>);
1201 let mut expanded_module = use_signal::<ExpandedModuleKey>(|| None);
1203 let mut collapsed_eras = use_signal(|| std::collections::HashSet::<String>::new());
1205
1206 let eras = get_curriculum_data();
1207
1208 let content_engine = ContentEngine::new();
1210 let user_progress = UserProgress::new(); 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 #[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 #[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 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 for i in 0..entries.length() {
1262 if let Ok(entry) = entries.get(i).dyn_into::<web_sys::IntersectionObserverEntry>() {
1263 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 let mut options = web_sys::IntersectionObserverInit::new();
1278 options.root_margin("-100px 0px -90% 0px");
1282 let thresholds = js_sys::Array::new();
1284 thresholds.push(&JsValue::from(0.0));
1285 options.threshold(&thresholds);
1286
1287 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 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 callback.forget();
1305 });
1306 }
1307
1308 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 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 div { class: "learn-layout",
1348 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 main { class: "learn-content",
1359 for era in eras.iter() {
1360 {
1361 let show_divider = !is_first_era;
1363 is_first_era = false;
1364
1365 rsx! {
1366 if show_divider {
1368 div { class: "learn-era-divider",
1369 h2 { "{era.title}" }
1370 }
1371 }
1372
1373 {
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 let _is_unlocked = true;
1436
1437 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 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 #[cfg(target_arch = "wasm32")]
1465 {
1466 let id = module_id_for_scroll.clone();
1467 wasm_bindgen_futures::spawn_local(async move {
1468 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 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; 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 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 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 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 VocabReference {}
1571
1572 Footer {}
1573 }
1574 }
1575}
1576
1577#[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#[derive(Debug, Clone, Copy, PartialEq, Default)]
1595enum PracticeMode {
1596 #[default]
1597 Practice,
1598 Test,
1599}
1600
1601#[derive(Debug, Clone, Copy, PartialEq, Default)]
1603enum ContentView {
1604 #[default]
1605 Lesson,
1606 Examples,
1607 Practice,
1608 Test,
1609}
1610
1611#[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 let mut content_view = use_signal(ContentView::default);
1630
1631 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 let mut exercise_attempts = use_signal(|| std::collections::HashMap::<usize, u32>::new());
1643 let mut completed_exercises = use_signal(|| std::collections::HashSet::<usize>::new());
1645 let mut answer_revealed_exercises = use_signal(|| std::collections::HashSet::<usize>::new());
1647
1648 let mut retry_queue = use_signal(|| std::collections::VecDeque::<usize>::new());
1651 let mut exercises_since_retry = use_signal(|| 0usize);
1653
1654 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 let mut struggle_detector = use_signal(StruggleDetector::default);
1661 let mut show_socratic_hint = use_signal(|| false);
1662
1663 let mut quiz_answers = use_signal(|| std::collections::HashMap::<String, (usize, bool)>::new());
1666
1667 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 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 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 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 let hint_text = current_challenge.as_ref().and_then(|ch| ch.hint.clone());
1710
1711 let test_total = 10usize;
1713 let is_test_mode = *practice_mode.read() == PracticeMode::Test;
1714 let current_test_q = *test_question.read();
1715
1716 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 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 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 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 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 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 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 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 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 if is_test_mode {
1961 if *test_complete.read() {
1962 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 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 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 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 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 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 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 let answer_was_revealed = answer_revealed_exercises.read().contains(¤t_idx);
2075 let already_completed = if is_test_mode { false } else { completed_exercises.read().contains(¤t_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(¤t_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 let attempts = exercise_attempts.read().get(¤t_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(¤t_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 let result = check_answer(&answer, expected);
2139 if result.correct {
2140 let answer_was_revealed = answer_revealed_exercises.read().contains(¤t_idx);
2142
2143 let already_completed = if is_test_mode {
2145 false } else {
2147 completed_exercises.read().contains(¤t_idx)
2148 };
2149
2150 if answer_was_revealed {
2151 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 let wrong_count = *exercise_attempts.read().get(¤t_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 let multiplier = match current_streak {
2168 0 => 1.0,
2169 1 => 1.25,
2170 2 => 1.5,
2171 3 => 1.75,
2172 _ => 2.0, };
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 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 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 let attempts = exercise_attempts.read().get(¤t_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 if !is_test_mode {
2219 let mut queue = retry_queue.write();
2220 if !queue.contains(¤t_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 let answer_was_revealed = answer_revealed_exercises.read().contains(¤t_idx);
2262
2263 let already_completed = if is_test_mode {
2265 false } else {
2267 completed_exercises.read().contains(¤t_idx)
2268 };
2269
2270 if answer_was_revealed {
2271 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 let wrong_count = *exercise_attempts.read().get(¤t_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 let multiplier = match current_streak {
2288 0 => 1.0,
2289 1 => 1.25,
2290 2 => 1.5,
2291 3 => 1.75,
2292 _ => 2.0, };
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 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 let attempts = exercise_attempts.read().get(¤t_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 if !is_test_mode {
2338 let mut queue = retry_queue.write();
2339 if !queue.contains(¤t_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 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 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 if !is_test_mode {
2394 div { class: "reveal-section",
2395 div { class: "reveal-buttons",
2396 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 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 answer_revealed_exercises.write().insert(current_idx);
2415 }
2416 reveal_state.write().answer = !current;
2417 },
2418 "✓ Show Answer (No XP)"
2419 }
2420
2421 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 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 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 } div { class: "learn-module-actions", style: "margin-top: 24px;",
2474 if is_test_mode {
2475 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 test_answers.write().push(is_correct);
2494
2495 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 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 button {
2521 class: "learn-action-btn secondary",
2522 onclick: {
2523 move |_| {
2524 let next = current_idx + 1;
2526 if next < total_exercises {
2527 current_exercise_idx.set(next);
2528 } else {
2529 current_exercise_idx.set(0); }
2531 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 let next = current_idx + 1;
2549 if next < total_exercises {
2550 current_exercise_idx.set(next);
2551 } else {
2552 current_exercise_idx.set(0); }
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 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 } }
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 } } }
2597 }
2598}