Skip to main content

logicaffeine_web/ui/components/
logic_output.rs

1//! First-Order Logic output display component.
2//!
3//! Renders compiled logic expressions with syntax highlighting. Supports multiple
4//! output formats (Unicode, SimpleFOL, LaTeX, Kripke) and multiple readings for
5//! ambiguous sentences.
6//!
7//! # Props
8//!
9//! - `logic` - Primary Unicode output
10//! - `simple_logic` - SimpleFOL format output
11//! - `kripke_logic` - Kripke semantics output
12//! - `readings` - Multiple readings for ambiguous sentences
13//! - `error` - Optional error message
14//! - `format` - Output format selection
15//!
16//! # Syntax Highlighting
17//!
18//! Different logical elements are color-coded:
19//! - Quantifiers (∀, ∃): purple
20//! - Connectives (∧, ∨, →, ↔, ¬): purple
21//! - Variables (lowercase): blue
22//! - Predicates (uppercase words): green
23//! - Constants (capitalized names): yellow
24
25use 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,  // Deep/Kripke semantics
164}
165
166#[component]
167pub fn LogicOutput(
168    logic: Option<String>,
169    simple_logic: Option<String>,
170    kripke_logic: Option<String>,  // Deep/Kripke semantics output (single)
171    readings: Vec<String>,
172    simple_readings: Vec<String>,   // SimpleFOL readings (deduplicated)
173    kripke_readings: Vec<String>,   // Deep/Kripke readings for ambiguous sentences
174    error: Option<String>,
175    format: OutputFormat,
176) -> Element {
177    let mut current_reading = use_signal(|| 0usize);
178
179    // Use format-appropriate readings
180    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}