logicaffeine_web/ui/components/
socratic_guide.rs1use 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}