Skip to main content

logicaffeine_web/ui/components/
ast_tree.rs

1//! Interactive AST (Abstract Syntax Tree) visualization.
2//!
3//! Renders a collapsible tree view of parsed expressions with syntax-highlighted
4//! node types. Useful for understanding sentence structure and debugging.
5//!
6//! # Props
7//!
8//! - `ast` - Optional AST node to display (shows placeholder if None)
9//!
10//! # Node Types
11//!
12//! Different node types are color-coded:
13//! - Quantifiers (purple): ∀, ∃
14//! - Predicates (green): Property names
15//! - Binary operators (purple): ∧, ∨, →, ↔
16//! - Variables (blue): x, y, z
17//! - Constants (yellow): Named entities
18
19use dioxus::prelude::*;
20use crate::AstNode;
21
22const TREE_STYLE: &str = r#"
23.ast-tree-container {
24    display: flex;
25    flex-direction: column;
26    height: 100%;
27    overflow: auto;
28    padding: 16px;
29    -webkit-overflow-scrolling: touch;
30}
31
32.ast-tree-empty {
33    color: #666;
34    font-style: italic;
35    text-align: center;
36    padding: 40px 20px;
37}
38
39.ast-node {
40    margin-left: 16px;
41    position: relative;
42}
43
44.ast-node:before {
45    content: '';
46    position: absolute;
47    left: -12px;
48    top: 0;
49    height: 100%;
50    width: 1px;
51    background: rgba(255, 255, 255, 0.1);
52}
53
54.ast-node:last-child:before {
55    height: 12px;
56}
57
58.ast-node-label {
59    display: flex;
60    align-items: center;
61    gap: 6px;
62    padding: 4px 8px;
63    border-radius: 4px;
64    cursor: pointer;
65    transition: background 0.15s ease;
66    position: relative;
67    -webkit-tap-highlight-color: transparent;
68}
69
70.ast-node-label:hover {
71    background: rgba(255, 255, 255, 0.05);
72}
73
74.ast-node-label:before {
75    content: '';
76    position: absolute;
77    left: -12px;
78    top: 50%;
79    width: 8px;
80    height: 1px;
81    background: rgba(255, 255, 255, 0.1);
82}
83
84.ast-node-toggle {
85    width: 16px;
86    height: 16px;
87    display: flex;
88    align-items: center;
89    justify-content: center;
90    font-size: 10px;
91    color: #666;
92    transition: transform 0.15s ease;
93}
94
95.ast-node-toggle.expanded {
96    transform: rotate(90deg);
97}
98
99.ast-node-text {
100    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
101    font-size: 13px;
102}
103
104.ast-node-type {
105    font-size: 10px;
106    padding: 2px 6px;
107    border-radius: 3px;
108    background: rgba(255, 255, 255, 0.08);
109    color: #888;
110    white-space: nowrap;
111}
112
113.ast-node-type.quantifier { background: rgba(198, 120, 221, 0.2); color: #c678dd; }
114.ast-node-type.predicate { background: rgba(152, 195, 121, 0.2); color: #98c379; }
115.ast-node-type.binary_op { background: rgba(198, 120, 221, 0.2); color: #c678dd; }
116.ast-node-type.unary_op { background: rgba(224, 108, 117, 0.2); color: #e06c75; }
117.ast-node-type.constant { background: rgba(229, 192, 123, 0.2); color: #e5c07b; }
118.ast-node-type.variable { background: rgba(97, 175, 239, 0.2); color: #61afef; }
119.ast-node-type.modal { background: rgba(86, 182, 194, 0.2); color: #56b6c2; }
120.ast-node-type.lambda { background: rgba(224, 108, 117, 0.2); color: #e06c75; }
121
122.ast-children {
123    display: none;
124}
125
126.ast-children.expanded {
127    display: block;
128}
129
130.ast-root {
131    margin-left: 0;
132}
133
134.ast-root:before {
135    display: none;
136}
137
138.ast-root > .ast-node-label:before {
139    display: none;
140}
141
142/* Mobile optimizations */
143@media (max-width: 768px) {
144    .ast-tree-container {
145        padding: 12px;
146    }
147
148    .ast-tree-empty {
149        padding: 30px 16px;
150        font-size: 14px;
151    }
152
153    /* Larger touch targets for tree nodes */
154    .ast-node {
155        margin-left: 14px;
156    }
157
158    .ast-node-label {
159        padding: 8px 10px;
160        gap: 8px;
161        min-height: 40px;
162        border-radius: 6px;
163    }
164
165    .ast-node-label:active {
166        background: rgba(255, 255, 255, 0.1);
167    }
168
169    .ast-node-toggle {
170        width: 24px;
171        height: 24px;
172        font-size: 12px;
173    }
174
175    .ast-node-text {
176        font-size: 14px;
177        word-break: break-word;
178    }
179
180    .ast-node-type {
181        font-size: 11px;
182        padding: 3px 8px;
183        border-radius: 4px;
184    }
185
186    .ast-node:before {
187        left: -10px;
188    }
189
190    .ast-node-label:before {
191        left: -10px;
192        width: 6px;
193    }
194}
195
196/* Extra small screens */
197@media (max-width: 480px) {
198    .ast-tree-container {
199        padding: 10px;
200    }
201
202    .ast-node {
203        margin-left: 12px;
204    }
205
206    .ast-node-label {
207        padding: 6px 8px;
208        min-height: 36px;
209    }
210
211    .ast-node-text {
212        font-size: 13px;
213    }
214
215    .ast-node-type {
216        font-size: 10px;
217        padding: 2px 6px;
218    }
219}
220"#;
221
222#[component]
223pub fn AstTree(ast: Option<AstNode>) -> Element {
224    rsx! {
225        style { "{TREE_STYLE}" }
226
227        div { class: "ast-tree-container",
228            if let Some(node) = ast {
229                AstNodeView { node: node, is_root: true }
230            } else {
231                div { class: "ast-tree-empty",
232                    "Parse a sentence to see its AST..."
233                }
234            }
235        }
236    }
237}
238
239#[component]
240fn AstNodeView(node: AstNode, is_root: bool) -> Element {
241    let mut expanded = use_signal(|| true);
242    let has_children = !node.children.is_empty();
243
244    let node_class = if is_root { "ast-node ast-root" } else { "ast-node" };
245    let toggle_class = if *expanded.read() { "ast-node-toggle expanded" } else { "ast-node-toggle" };
246    let children_class = if *expanded.read() { "ast-children expanded" } else { "ast-children" };
247    let type_class = format!("ast-node-type {}", node.node_type);
248
249    rsx! {
250        div { class: "{node_class}",
251            div {
252                class: "ast-node-label",
253                onclick: move |_| {
254                    if has_children {
255                        let current = *expanded.read();
256                        expanded.set(!current);
257                    }
258                },
259                if has_children {
260                    span { class: "{toggle_class}", "\u{25B6}" }
261                } else {
262                    span { class: "ast-node-toggle", "\u{2022}" }
263                }
264                span { class: "ast-node-text", "{node.label}" }
265                span { class: "{type_class}", "{node.node_type}" }
266            }
267
268            if has_children {
269                div { class: "{children_class}",
270                    for child in node.children.iter() {
271                        AstNodeView { node: child.clone(), is_root: false }
272                    }
273                }
274            }
275        }
276    }
277}