Skip to main content

logicaffeine_web/ui/components/
recall_block.rs

1//! End-of-section "Check yourself" retrieval cards (Roediger & Karpicke).
2//!
3//! Free recall with a reveal: the reader answers in their head, reveals, and
4//! self-marks — the mark feeds the SM-2 deck (`guide_review`) so the card
5//! comes back right when it is about to fade. The answer is CONDITIONALLY
6//! rendered, never CSS-hidden: a flashcard that leaks its back into the DOM
7//! teaches nothing (and the render lock proves it).
8
9use crate::guide_review::record_recall_mark;
10use crate::progress::UserProgress;
11use dioxus::prelude::*;
12
13#[derive(Props, Clone, PartialEq)]
14pub struct RecallCardProps {
15    pub id: String,
16    pub question: String,
17    pub answer: String,
18}
19
20#[component]
21pub fn RecallCard(props: RecallCardProps) -> Element {
22    let mut revealed = use_signal(|| false);
23    let mut marked = use_signal(|| false);
24
25    let progress_ctx = try_consume_context::<Signal<UserProgress>>();
26    let recall_id = props.id.clone();
27    let self_mark = move |quality: u8| {
28        let today = crate::ui::pages::guide::today_string();
29        match progress_ctx {
30            Some(mut progress) => {
31                record_recall_mark(&mut progress.write(), &recall_id, quality, &today);
32            }
33            None => {
34                let mut progress = UserProgress::load();
35                record_recall_mark(&mut progress, &recall_id, quality, &today);
36            }
37        }
38        marked.set(true);
39    };
40
41    rsx! {
42        div {
43            class: "guide-recall-card",
44            id: "{props.id}",
45            style: "border: 1px solid rgba(255,255,255,0.10); border-radius: 10px; padding: 12px 16px; background: rgba(255,255,255,0.03);",
46
47            div {
48                style: "display: flex; align-items: center; justify-content: space-between; gap: 12px;",
49                span {
50                    style: "font-size: 14px; color: rgba(229,231,235,0.9); line-height: 1.5;",
51                    "{props.question}"
52                }
53                if !*revealed.read() {
54                    button {
55                        class: "guide-recall-reveal",
56                        style: "flex: none; padding: 5px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); color: rgba(229,231,235,0.75); font-size: 12px; cursor: pointer;",
57                        onclick: move |_| revealed.set(true),
58                        "Reveal"
59                    }
60                }
61            }
62
63            if *revealed.read() {
64                div {
65                    style: "margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.08); font-size: 13px; color: #a78bfa; line-height: 1.6;",
66                    "{props.answer}"
67                }
68                if !*marked.read() {
69                    div {
70                        class: "guide-recall-marks",
71                        style: "display: flex; gap: 8px; margin-top: 10px; align-items: center;",
72                        span {
73                            style: "font-size: 11px; color: rgba(229,231,235,0.45);",
74                            "Did you have it?"
75                        }
76                        button {
77                            class: "guide-recall-mark",
78                            style: "padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(248,113,113,0.4); background: none; color: #f87171; font-size: 11px; cursor: pointer;",
79                            onclick: { let mut m = self_mark.clone(); move |_| m(1) },
80                            "Again"
81                        }
82                        button {
83                            class: "guide-recall-mark",
84                            style: "padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(251,191,36,0.4); background: none; color: #fbbf24; font-size: 11px; cursor: pointer;",
85                            onclick: { let mut m = self_mark.clone(); move |_| m(3) },
86                            "Hard"
87                        }
88                        button {
89                            class: "guide-recall-mark",
90                            style: "padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(96,165,250,0.4); background: none; color: #60a5fa; font-size: 11px; cursor: pointer;",
91                            onclick: { let mut m = self_mark.clone(); move |_| m(4) },
92                            "Good"
93                        }
94                        button {
95                            class: "guide-recall-mark",
96                            style: "padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(34,197,94,0.4); background: none; color: #22c55e; font-size: 11px; cursor: pointer;",
97                            onclick: { let mut m = self_mark.clone(); move |_| m(5) },
98                            "Easy"
99                        }
100                    }
101                } else {
102                    div {
103                        style: "margin-top: 10px; font-size: 11px; color: rgba(229,231,235,0.45);",
104                        "Scheduled — it will resurface on the Review page when it's about to fade."
105                    }
106                }
107            }
108        }
109    }
110}
111
112/// A section's recall cards under one "Check yourself" heading.
113#[component]
114pub fn RecallBlock(cards: Vec<(String, String, String)>) -> Element {
115    rsx! {
116        div {
117            class: "guide-recall",
118            style: "margin-top: 24px; display: flex; flex-direction: column; gap: 10px;",
119            div {
120                style: "font-size: 13px; font-weight: 700; color: rgba(229,231,235,0.85); text-transform: uppercase; letter-spacing: 0.5px;",
121                "Check yourself"
122            }
123            for (id, question, answer) in cards {
124                RecallCard { id, question, answer }
125            }
126        }
127    }
128}