logicaffeine_web/ui/components/
logic_output.rs1use dioxus::prelude::*;
26
27const OUTPUT_STYLE: &str = r#"
28.logic-output-container {
29 display: flex;
30 flex-direction: column;
31 padding: 16px;
32}
33
34.reading-selector {
35 display: flex;
36 align-items: center;
37 gap: 8px;
38 margin-bottom: 12px;
39 flex-wrap: wrap;
40}
41
42.reading-selector span {
43 color: #888;
44 font-size: 14px;
45}
46
47.reading-btn {
48 width: 28px;
49 height: 28px;
50 border-radius: 6px;
51 border: 1px solid rgba(255, 255, 255, 0.2);
52 background: rgba(255, 255, 255, 0.08);
53 color: #888;
54 font-size: 12px;
55 cursor: pointer;
56 transition: all 0.2s ease;
57}
58
59.reading-btn:hover {
60 background: rgba(255, 255, 255, 0.15);
61 color: #e8e8e8;
62}
63
64.reading-btn.active {
65 background: #667eea;
66 border-color: transparent;
67 color: white;
68}
69
70.logic-display {
71 background: rgba(255, 255, 255, 0.08);
72 border: 1px solid rgba(255, 255, 255, 0.2);
73 border-radius: 12px;
74 padding: 16px;
75 font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
76 font-size: 16px;
77 line-height: 1.6;
78 color: #e8e8e8;
79 overflow: auto;
80 -webkit-overflow-scrolling: touch;
81 word-break: break-word;
82}
83
84.logic-display.empty {
85 color: #666;
86 font-style: italic;
87 padding: 16px;
88}
89
90.logic-display.error {
91 border-color: rgba(224, 108, 117, 0.3);
92 color: #e06c75;
93}
94
95.logic-quantifier { color: #c678dd; font-weight: 500; }
96.logic-variable { color: #61afef; }
97.logic-predicate { color: #98c379; }
98.logic-connective { color: #c678dd; }
99.logic-constant { color: #e5c07b; }
100.logic-paren { color: #abb2bf; }
101
102/* Mobile optimizations */
103@media (max-width: 768px) {
104 .logic-output-container {
105 padding: 12px;
106 }
107
108 .reading-selector {
109 gap: 6px;
110 margin-bottom: 10px;
111 }
112
113 .reading-selector span {
114 font-size: 13px;
115 }
116
117 /* Touch-friendly reading buttons */
118 .reading-btn {
119 width: 44px;
120 height: 44px;
121 font-size: 14px;
122 border-radius: 8px;
123 -webkit-tap-highlight-color: transparent;
124 }
125
126 .reading-btn:active {
127 transform: scale(0.95);
128 }
129
130 .logic-display {
131 padding: 16px;
132 font-size: 16px;
133 border-radius: 10px;
134 }
135
136 .logic-display.empty {
137 font-size: 14px;
138 padding: 16px;
139 }
140}
141
142/* Extra small screens */
143@media (max-width: 480px) {
144 .reading-btn {
145 width: 40px;
146 height: 40px;
147 font-size: 13px;
148 }
149
150 .logic-display {
151 font-size: 15px;
152 padding: 14px;
153 }
154}
155"#;
156
157#[derive(Clone, Copy, PartialEq, Default)]
158pub enum OutputFormat {
159 #[default]
160 Unicode,
161 SimpleFOL,
162 LaTeX,
163 Kripke, }
165
166#[component]
167pub fn LogicOutput(
168 logic: Option<String>,
169 simple_logic: Option<String>,
170 kripke_logic: Option<String>, readings: Vec<String>,
172 simple_readings: Vec<String>, kripke_readings: Vec<String>, error: Option<String>,
175 format: OutputFormat,
176) -> Element {
177 let mut current_reading = use_signal(|| 0usize);
178
179 let active_readings = match format {
181 OutputFormat::Kripke => &kripke_readings,
182 OutputFormat::SimpleFOL => &simple_readings,
183 _ => &readings,
184 };
185
186 let total_readings = active_readings.len().max(1);
187 let display_logic = if !active_readings.is_empty() {
188 let idx = (*current_reading.read()).min(active_readings.len().saturating_sub(1));
189 Some(active_readings.get(idx).cloned().unwrap_or_default())
190 } else if format == OutputFormat::Kripke {
191 kripke_logic.clone()
192 } else {
193 logic.clone()
194 };
195
196 let formatted_output = match format {
197 OutputFormat::LaTeX => display_logic.as_ref().map(|l| convert_to_latex(l)).unwrap_or_default(),
198 _ => display_logic.clone().unwrap_or_default(),
199 };
200
201 rsx! {
202 style { "{OUTPUT_STYLE}" }
203
204 div { class: "logic-output-container",
205 if total_readings > 1 {
206 div { class: "reading-selector",
207 span { "Reading" }
208 for i in 0..total_readings {
209 button {
210 class: if *current_reading.read() == i { "reading-btn active" } else { "reading-btn" },
211 onclick: move |_| current_reading.set(i),
212 "{i + 1}"
213 }
214 }
215 span { "of {total_readings}" }
216 }
217 }
218
219 if let Some(err) = &error {
220 div { class: "logic-display error",
221 "{err}"
222 }
223 } else if formatted_output.is_empty() {
224 div { class: "logic-display empty",
225 "Type a sentence to see its logical form..."
226 }
227 } else {
228 div { class: "logic-display",
229 dangerous_inner_html: highlight_logic(&formatted_output)
230 }
231 }
232 }
233 }
234}
235
236fn convert_to_latex(unicode: &str) -> String {
237 unicode
238 .replace('\u{2200}', "\\forall ")
239 .replace('\u{2203}', "\\exists ")
240 .replace('\u{00AC}', "\\neg ")
241 .replace('\u{2227}', "\\land ")
242 .replace('\u{2228}', "\\lor ")
243 .replace('\u{2192}', "\\rightarrow ")
244 .replace('\u{2194}', "\\leftrightarrow ")
245 .replace('\u{22A5}', "\\bot ")
246 .replace('\u{22A4}', "\\top ")
247}
248
249pub fn highlight_logic(logic: &str) -> String {
250 let mut result = String::new();
251 let mut chars = logic.chars().peekable();
252
253 while let Some(c) = chars.next() {
254 match c {
255 '\u{2200}' | '\u{2203}' => {
256 result.push_str(&format!(r#"<span class="logic-quantifier">{}</span>"#, c));
257 }
258 '\u{00AC}' | '\u{2227}' | '\u{2228}' | '\u{2192}' | '\u{2194}' => {
259 result.push_str(&format!(r#"<span class="logic-connective">{}</span>"#, c));
260 }
261 '(' | ')' | '[' | ']' => {
262 result.push_str(&format!(r#"<span class="logic-paren">{}</span>"#, c));
263 }
264 'a'..='z' if chars.peek().map(|n| !n.is_alphabetic()).unwrap_or(true) => {
265 result.push_str(&format!(r#"<span class="logic-variable">{}</span>"#, c));
266 }
267 'A'..='Z' => {
268 let mut word = String::from(c);
269 while let Some(&next) = chars.peek() {
270 if next.is_alphanumeric() {
271 word.push(chars.next().unwrap());
272 } else {
273 break;
274 }
275 }
276 if word.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
277 && word.len() > 1
278 && word.chars().skip(1).all(|c| c.is_lowercase() || c.is_numeric())
279 {
280 result.push_str(&format!(r#"<span class="logic-constant">{}</span>"#, word));
281 } else {
282 result.push_str(&format!(r#"<span class="logic-predicate">{}</span>"#, word));
283 }
284 }
285 '\n' => result.push_str("<br>"),
286 _ => result.push(c),
287 }
288 }
289
290 result
291}