Skip to main content

logicaffeine_web/ui/components/
vocab_reference.rs

1//! Vocabulary Reference component
2//!
3//! A collapsible reference panel showing all common logic symbols
4//! and vocabulary terms, accessible from anywhere in the learning experience.
5
6use dioxus::prelude::*;
7use crate::ui::components::icon::{Icon, IconVariant, IconSize};
8
9const VOCAB_REFERENCE_STYLE: &str = r#"
10.vocab-reference-toggle {
11    position: fixed;
12    bottom: 24px;
13    right: 24px;
14    width: 48px;
15    height: 48px;
16    border-radius: 50%;
17    background: #667eea;
18    border: none;
19    color: #fff;
20    font-size: 24px;
21    cursor: pointer;
22    display: flex;
23    align-items: center;
24    justify-content: center;
25    box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
26    transition: transform 0.2s ease, box-shadow 0.2s ease;
27    z-index: 1000;
28}
29
30.vocab-reference-toggle:hover {
31    transform: scale(1.05);
32    box-shadow: 0 6px 24px rgba(96, 165, 250, 0.4);
33}
34
35.vocab-reference-toggle.active {
36    background: rgba(255, 255, 255, 0.1);
37    color: var(--text-primary);
38}
39
40/* Attention animation - pulses after delay */
41.vocab-reference-toggle.attention {
42    animation: attentionPulse 2s ease-in-out 3;
43}
44
45@keyframes attentionPulse {
46    0%, 100% {
47        transform: scale(1);
48        box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
49    }
50    50% {
51        transform: scale(1.15);
52        box-shadow: 0 6px 30px rgba(102, 126, 234, 0.6);
53    }
54}
55
56.vocab-reference-panel {
57    position: fixed;
58    bottom: 84px;
59    right: 24px;
60    width: 360px;
61    max-height: 70vh;
62    background: #12161c;
63    border: 1px solid rgba(255, 255, 255, 0.12);
64    border-radius: 12px;
65    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
66    z-index: 999;
67    overflow: hidden;
68    animation: slideUp 0.2s ease;
69}
70
71@keyframes slideUp {
72    from {
73        opacity: 0;
74        transform: translateY(16px);
75    }
76    to {
77        opacity: 1;
78        transform: translateY(0);
79    }
80}
81
82.vocab-panel-header {
83    display: flex;
84    align-items: center;
85    justify-content: space-between;
86    padding: var(--spacing-lg);
87    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
88    background: rgba(255, 255, 255, 0.03);
89}
90
91.vocab-panel-title {
92    font-size: var(--font-body-lg);
93    font-weight: 700;
94    color: var(--text-primary);
95    display: flex;
96    align-items: center;
97    gap: var(--spacing-sm);
98}
99
100.vocab-panel-close {
101    width: 28px;
102    height: 28px;
103    border-radius: 50%;
104    background: rgba(255, 255, 255, 0.08);
105    border: 1px solid rgba(255, 255, 255, 0.12);
106    color: var(--text-secondary);
107    font-size: 14px;
108    cursor: pointer;
109    display: flex;
110    align-items: center;
111    justify-content: center;
112    transition: all 0.15s ease;
113}
114
115.vocab-panel-close:hover {
116    background: rgba(255, 255, 255, 0.15);
117    color: var(--text-primary);
118}
119
120.vocab-panel-content {
121    padding: var(--spacing-lg);
122    max-height: calc(70vh - 60px);
123    overflow-y: auto;
124}
125
126.vocab-section {
127    margin-bottom: var(--spacing-xl);
128}
129
130.vocab-section:last-child {
131    margin-bottom: 0;
132}
133
134.vocab-section-title {
135    font-size: var(--font-caption-md);
136    font-weight: 600;
137    text-transform: uppercase;
138    letter-spacing: 0.5px;
139    margin-bottom: var(--spacing-md);
140    display: flex;
141    align-items: center;
142    gap: var(--spacing-xs);
143}
144
145.vocab-section-title.quantifiers {
146    color: #60a5fa;
147}
148
149.vocab-section-title.connectives {
150    color: #f472b6;
151}
152
153.vocab-section-title.predicates {
154    color: #4ade80;
155}
156
157.vocab-section-title.terms {
158    color: #a78bfa;
159}
160
161.vocab-items {
162    display: flex;
163    flex-direction: column;
164    gap: var(--spacing-xs);
165}
166
167.vocab-item {
168    display: flex;
169    align-items: center;
170    gap: var(--spacing-md);
171    padding: var(--spacing-sm) var(--spacing-md);
172    background: rgba(255, 255, 255, 0.03);
173    border-radius: var(--radius-md);
174    transition: background 0.15s ease;
175}
176
177.vocab-item:hover {
178    background: rgba(255, 255, 255, 0.06);
179}
180
181.vocab-symbol {
182    font-family: var(--font-mono);
183    font-size: var(--font-body-lg);
184    min-width: 36px;
185    text-align: center;
186    font-weight: 600;
187}
188
189.vocab-symbol.quantifier {
190    color: #60a5fa;
191}
192
193.vocab-symbol.connective {
194    color: #f472b6;
195}
196
197.vocab-symbol.predicate {
198    color: #4ade80;
199}
200
201.vocab-info {
202    flex: 1;
203}
204
205.vocab-name {
206    font-size: var(--font-body-sm);
207    font-weight: 600;
208    color: var(--text-primary);
209}
210
211.vocab-meaning {
212    font-size: var(--font-caption-md);
213    color: var(--text-tertiary);
214}
215
216.vocab-term-item {
217    padding: var(--spacing-md);
218    background: rgba(255, 255, 255, 0.03);
219    border-radius: var(--radius-md);
220    margin-bottom: var(--spacing-sm);
221}
222
223.vocab-term-name {
224    font-weight: 600;
225    color: var(--color-accent-purple);
226    margin-bottom: var(--spacing-xs);
227}
228
229.vocab-term-def {
230    font-size: var(--font-body-sm);
231    color: var(--text-secondary);
232    line-height: 1.5;
233}
234
235/* Search box */
236.vocab-search {
237    margin-bottom: var(--spacing-lg);
238}
239
240.vocab-search-input {
241    width: 100%;
242    padding: var(--spacing-sm) var(--spacing-md);
243    background: rgba(255, 255, 255, 0.06);
244    border: 1px solid rgba(255, 255, 255, 0.12);
245    border-radius: var(--radius-md);
246    color: var(--text-primary);
247    font-size: var(--font-body-sm);
248    outline: none;
249    transition: border-color 0.2s ease;
250}
251
252.vocab-search-input:focus {
253    border-color: var(--color-accent-blue);
254}
255
256.vocab-search-input::placeholder {
257    color: var(--text-tertiary);
258}
259"#;
260
261/// Symbol entry for the reference
262struct SymbolRef {
263    symbol: &'static str,
264    name: &'static str,
265    meaning: &'static str,
266    category: &'static str,
267}
268
269/// Vocabulary term entry
270struct VocabTerm {
271    term: &'static str,
272    definition: &'static str,
273}
274
275/// Get all reference symbols
276fn get_symbols() -> Vec<SymbolRef> {
277    vec![
278        // Quantifiers
279        SymbolRef { symbol: "∀", name: "Universal", meaning: "for all", category: "quantifier" },
280        SymbolRef { symbol: "∃", name: "Existential", meaning: "there exists", category: "quantifier" },
281        SymbolRef { symbol: "∃!", name: "Unique", meaning: "there exists exactly one", category: "quantifier" },
282        // Connectives
283        SymbolRef { symbol: "∧", name: "Conjunction", meaning: "and", category: "connective" },
284        SymbolRef { symbol: "∨", name: "Disjunction", meaning: "or", category: "connective" },
285        SymbolRef { symbol: "→", name: "Implication", meaning: "if...then", category: "connective" },
286        SymbolRef { symbol: "↔", name: "Biconditional", meaning: "if and only if", category: "connective" },
287        SymbolRef { symbol: "¬", name: "Negation", meaning: "not", category: "connective" },
288        // Modal
289        SymbolRef { symbol: "□", name: "Necessity", meaning: "necessarily", category: "connective" },
290        SymbolRef { symbol: "◇", name: "Possibility", meaning: "possibly", category: "connective" },
291        // Identity
292        SymbolRef { symbol: "=", name: "Identity", meaning: "equals", category: "predicate" },
293        SymbolRef { symbol: "≠", name: "Non-identity", meaning: "not equal to", category: "predicate" },
294    ]
295}
296
297/// Get vocabulary terms
298fn get_vocab_terms() -> Vec<VocabTerm> {
299    vec![
300        VocabTerm {
301            term: "Predicate",
302            definition: "A property or relation, written as a capitalized name with arguments in parentheses. E.g., Happy(alice)"
303        },
304        VocabTerm {
305            term: "Constant",
306            definition: "A name for a specific individual, written in lowercase. E.g., alice, bob, socrates"
307        },
308        VocabTerm {
309            term: "Variable",
310            definition: "A placeholder that can refer to any individual, typically x, y, z"
311        },
312        VocabTerm {
313            term: "Valid Argument",
314            definition: "An argument where the conclusion necessarily follows from the premises"
315        },
316        VocabTerm {
317            term: "Sound Argument",
318            definition: "A valid argument with premises that are actually true"
319        },
320        VocabTerm {
321            term: "Domain",
322            definition: "The set of all objects that variables can refer to"
323        },
324        VocabTerm {
325            term: "Scope",
326            definition: "The part of a formula to which a quantifier or connective applies"
327        },
328    ]
329}
330
331/// Props for VocabReference
332#[derive(Props, Clone, PartialEq)]
333pub struct VocabReferenceProps {
334    /// Whether to show the panel initially
335    #[props(default = false)]
336    pub initial_open: bool,
337}
338
339/// Vocabulary Reference floating panel
340#[component]
341pub fn VocabReference(props: VocabReferenceProps) -> Element {
342    let mut is_open = use_signal(|| props.initial_open);
343    let mut search_query = use_signal(|| String::new());
344    let mut show_attention = use_signal(|| false);
345
346    // Trigger attention animation after 10 seconds if not opened
347    use_effect(move || {
348        spawn(async move {
349            // Wait 10 seconds
350            gloo_timers::future::TimeoutFuture::new(10_000).await;
351            // Only show attention if panel hasn't been opened
352            if !*is_open.peek() {
353                show_attention.set(true);
354            }
355        });
356    });
357
358    let symbols = get_symbols();
359    let vocab_terms = get_vocab_terms();
360
361    // Filter based on search
362    let query = search_query.read().to_lowercase();
363    let filtered_symbols: Vec<_> = if query.is_empty() {
364        symbols.iter().collect()
365    } else {
366        symbols.iter().filter(|s| {
367            s.symbol.to_lowercase().contains(&query) ||
368            s.name.to_lowercase().contains(&query) ||
369            s.meaning.to_lowercase().contains(&query)
370        }).collect()
371    };
372
373    let filtered_terms: Vec<_> = if query.is_empty() {
374        vocab_terms.iter().collect()
375    } else {
376        vocab_terms.iter().filter(|t| {
377            t.term.to_lowercase().contains(&query) ||
378            t.definition.to_lowercase().contains(&query)
379        }).collect()
380    };
381
382    // Group symbols by category
383    let quantifiers: Vec<_> = filtered_symbols.iter().filter(|s| s.category == "quantifier").collect();
384    let connectives: Vec<_> = filtered_symbols.iter().filter(|s| s.category == "connective").collect();
385    let predicates: Vec<_> = filtered_symbols.iter().filter(|s| s.category == "predicate").collect();
386
387    rsx! {
388        style { "{VOCAB_REFERENCE_STYLE}" }
389
390        // Toggle button
391        button {
392            class: {
393                let open = *is_open.read();
394                let attention = *show_attention.read();
395                if open {
396                    "vocab-reference-toggle active"
397                } else if attention {
398                    "vocab-reference-toggle attention"
399                } else {
400                    "vocab-reference-toggle"
401                }
402            },
403            onclick: move |_| {
404                let current = *is_open.read();
405                is_open.set(!current);
406                // Stop attention animation once clicked
407                show_attention.set(false);
408            },
409            title: "Symbol & Vocabulary Reference",
410            if *is_open.read() {
411                Icon { variant: IconVariant::Close, size: IconSize::Large }
412            } else {
413                Icon { variant: IconVariant::Book, size: IconSize::Large }
414            }
415        }
416
417        // Panel
418        if *is_open.read() {
419            div { class: "vocab-reference-panel",
420                // Header
421                div { class: "vocab-panel-header",
422                    div { class: "vocab-panel-title",
423                        Icon { variant: IconVariant::Book, size: IconSize::Medium }
424                        "Reference"
425                    }
426                    button {
427                        class: "vocab-panel-close",
428                        onclick: move |_| is_open.set(false),
429                        "×"
430                    }
431                }
432
433                // Content
434                div { class: "vocab-panel-content",
435                    // Search
436                    div { class: "vocab-search",
437                        input {
438                            class: "vocab-search-input",
439                            r#type: "text",
440                            placeholder: "Search symbols or terms...",
441                            value: "{search_query}",
442                            oninput: move |e| search_query.set(e.value()),
443                        }
444                    }
445
446                    // Quantifiers
447                    if !quantifiers.is_empty() {
448                        div { class: "vocab-section",
449                            div { class: "vocab-section-title quantifiers", "Quantifiers" }
450                            div { class: "vocab-items",
451                                for sym in quantifiers {
452                                    div { class: "vocab-item",
453                                        span { class: "vocab-symbol quantifier", "{sym.symbol}" }
454                                        div { class: "vocab-info",
455                                            div { class: "vocab-name", "{sym.name}" }
456                                            div { class: "vocab-meaning", "{sym.meaning}" }
457                                        }
458                                    }
459                                }
460                            }
461                        }
462                    }
463
464                    // Connectives
465                    if !connectives.is_empty() {
466                        div { class: "vocab-section",
467                            div { class: "vocab-section-title connectives", "Connectives" }
468                            div { class: "vocab-items",
469                                for sym in connectives {
470                                    div { class: "vocab-item",
471                                        span { class: "vocab-symbol connective", "{sym.symbol}" }
472                                        div { class: "vocab-info",
473                                            div { class: "vocab-name", "{sym.name}" }
474                                            div { class: "vocab-meaning", "{sym.meaning}" }
475                                        }
476                                    }
477                                }
478                            }
479                        }
480                    }
481
482                    // Predicates
483                    if !predicates.is_empty() {
484                        div { class: "vocab-section",
485                            div { class: "vocab-section-title predicates", "Predicates & Relations" }
486                            div { class: "vocab-items",
487                                for sym in predicates {
488                                    div { class: "vocab-item",
489                                        span { class: "vocab-symbol predicate", "{sym.symbol}" }
490                                        div { class: "vocab-info",
491                                            div { class: "vocab-name", "{sym.name}" }
492                                            div { class: "vocab-meaning", "{sym.meaning}" }
493                                        }
494                                    }
495                                }
496                            }
497                        }
498                    }
499
500                    // Vocabulary Terms
501                    if !filtered_terms.is_empty() {
502                        div { class: "vocab-section",
503                            div { class: "vocab-section-title terms", "Key Terms" }
504                            for term in filtered_terms {
505                                div { class: "vocab-term-item",
506                                    div { class: "vocab-term-name", "{term.term}" }
507                                    div { class: "vocab-term-def", "{term.definition}" }
508                                }
509                            }
510                        }
511                    }
512                }
513            }
514        }
515    }
516}