Skip to main content

logicaffeine_web/ui/components/
socratic_guide.rs

1//! Socratic guide component for hints and feedback.
2//!
3//! Displays contextual hints and error messages using a Socratic teaching approach.
4//! Guides users toward understanding rather than providing direct answers.
5//!
6//! # Props
7//!
8//! - `mode` - Current guide state (Idle, Success, Error, Hint, Info)
9//! - `on_hint_request` - Optional callback for "Show me a hint" button
10//!
11//! # Modes
12//!
13//! - `Idle` - Placeholder when no input
14//! - `Success` - Green message for successful parsing
15//! - `Error` - Red message with Socratic guidance
16//! - `Hint` - Green hint text
17//! - `Info` - Blue informational message
18
19use dioxus::prelude::*;
20
21const GUIDE_STYLE: &str = r#"
22.socratic-guide {
23    display: flex;
24    align-items: center;
25    gap: 12px;
26    padding: 12px 20px;
27    background: transparent;
28    min-height: 44px;
29}
30
31.guide-content {
32    flex: 1;
33    display: flex;
34    align-items: center;
35    gap: 10px;
36}
37
38.guide-label {
39    font-size: 13px;
40    font-weight: 600;
41    text-transform: uppercase;
42    letter-spacing: 0.5px;
43    color: #667eea;
44    white-space: nowrap;
45}
46
47.guide-message {
48    font-size: 16px;
49    line-height: 1.5;
50    color: #e0e0e0;
51}
52
53.guide-message.error {
54    color: #e06c75;
55}
56
57.guide-message.hint {
58    color: #98c379;
59}
60
61.guide-message.info {
62    color: #61afef;
63}
64
65.guide-message code {
66    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
67    background: rgba(255, 255, 255, 0.08);
68    padding: 2px 6px;
69    border-radius: 4px;
70    font-size: 15px;
71}
72
73.guide-actions {
74    display: flex;
75    gap: 8px;
76    margin-top: 8px;
77}
78
79.guide-btn {
80    padding: 6px 12px;
81    border-radius: 6px;
82    border: 1px solid rgba(255, 255, 255, 0.15);
83    background: rgba(255, 255, 255, 0.05);
84    color: #888;
85    font-size: 12px;
86    cursor: pointer;
87    transition: all 0.2s ease;
88}
89
90.guide-btn:hover {
91    background: rgba(255, 255, 255, 0.1);
92    color: #e8e8e8;
93}
94
95.guide-btn.primary {
96    background: linear-gradient(135deg, #667eea, #764ba2);
97    border-color: transparent;
98    color: white;
99}
100
101.guide-btn.primary:hover {
102    opacity: 0.9;
103}
104
105.guide-empty {
106    color: #666;
107    font-style: italic;
108}
109"#;
110
111#[derive(Clone, PartialEq)]
112pub enum GuideMode {
113    Idle,
114    Success(String),
115    Error(String),
116    Hint(String),
117    Info(String),
118}
119
120impl Default for GuideMode {
121    fn default() -> Self {
122        GuideMode::Idle
123    }
124}
125
126#[component]
127pub fn SocraticGuide(
128    mode: GuideMode,
129    on_hint_request: Option<EventHandler<()>>,
130) -> Element {
131    let (message_class, label, message) = match &mode {
132        GuideMode::Idle => {
133            return rsx! {
134                style { "{GUIDE_STYLE}" }
135                div { class: "socratic-guide",
136                    div { class: "guide-content",
137                        div { class: "guide-message guide-empty",
138                            "Type an English sentence to translate it to First-Order Logic"
139                        }
140                    }
141                }
142            }
143        }
144        GuideMode::Success(msg) => ("guide-message", "Analysis", msg.clone()),
145        GuideMode::Error(msg) => ("guide-message error", "Something to consider", msg.clone()),
146        GuideMode::Hint(msg) => ("guide-message hint", "Hint", msg.clone()),
147        GuideMode::Info(msg) => ("guide-message info", "Observation", msg.clone()),
148    };
149
150    rsx! {
151        style { "{GUIDE_STYLE}" }
152
153        div { class: "socratic-guide",
154            div { class: "guide-content",
155                div { class: "guide-label", "{label}" }
156                div { class: "{message_class}",
157                    dangerous_inner_html: format_guide_message(&message)
158                }
159                if on_hint_request.is_some() && matches!(mode, GuideMode::Error(_)) {
160                    div { class: "guide-actions",
161                        button {
162                            class: "guide-btn",
163                            onclick: move |_| {
164                                if let Some(handler) = &on_hint_request {
165                                    handler.call(());
166                                }
167                            },
168                            "Show me a hint"
169                        }
170                    }
171                }
172            }
173        }
174    }
175}
176
177fn format_guide_message(message: &str) -> String {
178    let mut result = message.to_string();
179
180    let code_patterns = [
181        ("\u{2200}", "<code>\u{2200}</code>"),
182        ("\u{2203}", "<code>\u{2203}</code>"),
183        ("\u{2227}", "<code>\u{2227}</code>"),
184        ("\u{2228}", "<code>\u{2228}</code>"),
185        ("\u{2192}", "<code>\u{2192}</code>"),
186        ("\u{00AC}", "<code>\u{00AC}</code>"),
187    ];
188
189    for (pattern, replacement) in &code_patterns {
190        result = result.replace(pattern, replacement);
191    }
192
193    result
194}
195
196pub fn get_success_message(readings_count: usize) -> String {
197    match readings_count {
198        0 => "This sentence could not be resolved into a logical form.".to_string(),
199        1 => "This sentence has a single, unambiguous logical form.".to_string(),
200        2 => format!(
201            "This sentence is ambiguous \u{2014} {} different readings found. \
202            Click the reading buttons above to explore each interpretation.",
203            readings_count
204        ),
205        n => format!(
206            "{} distinct logical readings found. \
207            Each represents a valid interpretation of your input.",
208            n
209        ),
210    }
211}
212
213pub fn get_context_hint(input: &str) -> Option<String> {
214    let lower = input.to_lowercase();
215
216    if lower.starts_with("every") || lower.starts_with("all") {
217        Some("Universal quantification (\u{2200}) asserts something about ALL members of a set.".to_string())
218    } else if lower.starts_with("some") || lower.starts_with("a ") {
219        Some("Existential quantification (\u{2203}) asserts the EXISTENCE of at least one entity.".to_string())
220    } else if lower.contains(" loves ") || lower.contains(" sees ") {
221        Some("Transitive verbs create two-place predicates relating a subject to an object.".to_string())
222    } else if lower.contains(" is ") && lower.contains(" not ") {
223        Some("Negation (\u{00AC}) inverts the truth value of the proposition it scopes over.".to_string())
224    } else {
225        None
226    }
227}