Skip to main content

logicaffeine_web/ui/components/
proof_panel.rs

1//! Proof panel for Logic mode.
2//!
3//! Displays proof derivation trees and provides tactic buttons for guiding
4//! proofs. Shows proof status (idle, proving, success, failed) and optional
5//! Socratic hints.
6//!
7//! # Props
8//!
9//! - `proof_text` - Formatted derivation tree HTML
10//! - `status` - Current proof state
11//! - `hint` - Optional Socratic guidance text
12//! - `on_tactic` - Callback when a tactic button is clicked
13//!
14//! # Tactics
15//!
16//! - **Auto**: Automatic proof search
17//! - **Modus Ponens**: From P→Q and P, derive Q
18//! - **āˆ€ Elim**: Universal instantiation
19//! - **∃ Intro**: Existential introduction
20//! - **Induction**: Structural induction on Nat
21//! - **Rewrite**: Term substitution using equality
22
23use dioxus::prelude::*;
24
25const PROOF_PANEL_STYLE: &str = r#"
26.proof-panel {
27    display: flex;
28    flex-direction: column;
29    height: 100%;
30    background: #0f1419;
31    font-family: 'SF Mono', 'Fira Code', monospace;
32}
33
34.proof-header {
35    padding: 12px 16px;
36    background: rgba(255, 255, 255, 0.02);
37    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
38    display: flex;
39    align-items: center;
40    gap: 12px;
41}
42
43.proof-status {
44    font-size: 13px;
45    color: rgba(255, 255, 255, 0.7);
46}
47
48.proof-status.success {
49    color: #98c379;
50}
51
52.proof-status.error {
53    color: #e06c75;
54}
55
56.proof-status.pending {
57    color: #e5c07b;
58}
59
60/* Tactics bar */
61.tactics-bar {
62    display: flex;
63    flex-wrap: wrap;
64    gap: 6px;
65    padding: 12px 16px;
66    background: rgba(255, 255, 255, 0.02);
67    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
68}
69
70.tactic-btn {
71    padding: 6px 12px;
72    background: rgba(255, 255, 255, 0.04);
73    border: 1px solid rgba(255, 255, 255, 0.1);
74    border-radius: 4px;
75    color: rgba(255, 255, 255, 0.8);
76    font-size: 12px;
77    cursor: pointer;
78    transition: all 0.15s ease;
79}
80
81.tactic-btn:hover {
82    background: rgba(102, 126, 234, 0.15);
83    border-color: rgba(102, 126, 234, 0.3);
84    color: #667eea;
85}
86
87.tactic-btn.active {
88    background: rgba(102, 126, 234, 0.2);
89    border-color: #667eea;
90    color: #667eea;
91}
92
93/* Derivation tree */
94.derivation-tree {
95    flex: 1;
96    overflow: auto;
97    padding: 16px;
98    white-space: pre-wrap;
99    font-size: 13px;
100    line-height: 1.8;
101    color: #e8eaed;
102}
103
104.derivation-tree .rule {
105    color: #667eea;
106    font-weight: 600;
107}
108
109.derivation-tree .conclusion {
110    color: #98c379;
111}
112
113/* Socratic hint */
114.socratic-hint {
115    padding: 12px 16px;
116    background: rgba(229, 192, 123, 0.1);
117    border-top: 1px solid rgba(229, 192, 123, 0.2);
118    color: #e5c07b;
119    font-size: 13px;
120    display: flex;
121    align-items: center;
122    gap: 8px;
123}
124
125.hint-icon {
126    font-size: 16px;
127}
128
129/* Empty state */
130.proof-empty {
131    flex: 1;
132    display: flex;
133    flex-direction: column;
134    align-items: center;
135    justify-content: center;
136    color: rgba(255, 255, 255, 0.4);
137    text-align: center;
138    padding: 40px 20px;
139}
140
141.proof-empty-icon {
142    font-size: 48px;
143    margin-bottom: 16px;
144    opacity: 0.5;
145}
146
147.proof-empty-text {
148    font-size: 14px;
149    line-height: 1.6;
150}
151
152/* Mobile */
153@media (max-width: 768px) {
154    .tactics-bar {
155        padding: 10px 12px;
156        gap: 4px;
157    }
158
159    .tactic-btn {
160        padding: 8px 10px;
161        font-size: 11px;
162    }
163
164    .derivation-tree {
165        padding: 12px;
166        font-size: 12px;
167    }
168}
169"#;
170
171/// Available proof tactics for the UI
172#[derive(Clone, Copy, PartialEq, Eq)]
173pub enum Tactic {
174    Auto,
175    ModusPonens,
176    UniversalInst,
177    ExistentialIntro,
178    Induction,
179    Rewrite,
180}
181
182impl Tactic {
183    pub fn label(&self) -> &'static str {
184        match self {
185            Tactic::Auto => "Auto",
186            Tactic::ModusPonens => "Modus Ponens",
187            Tactic::UniversalInst => "\u{2200} Elim",
188            Tactic::ExistentialIntro => "\u{2203} Intro",
189            Tactic::Induction => "Induction",
190            Tactic::Rewrite => "Rewrite",
191        }
192    }
193
194    pub fn description(&self) -> &'static str {
195        match self {
196            Tactic::Auto => "Let the prover find a proof automatically",
197            Tactic::ModusPonens => "From P\u{2192}Q and P, derive Q",
198            Tactic::UniversalInst => "From \u{2200}x.P(x), derive P(c) for some c",
199            Tactic::ExistentialIntro => "From P(c), derive \u{2203}x.P(x)",
200            Tactic::Induction => "Prove by structural induction on Nat",
201            Tactic::Rewrite => "Use an equality to substitute terms",
202        }
203    }
204}
205
206/// Proof status for the UI
207#[derive(Clone, PartialEq, Default)]
208pub enum ProofStatus {
209    #[default]
210    Idle,
211    Proving,
212    Success,
213    Failed(String),
214}
215
216/// Proof panel component - displays proof status, tactics, and derivation tree
217#[component]
218pub fn ProofPanel(
219    /// The derivation tree as a formatted string (use DerivationTree::display_tree())
220    proof_text: String,
221    /// Current proof status
222    status: ProofStatus,
223    /// Optional Socratic hint
224    hint: Option<String>,
225    /// Callback when a tactic button is clicked
226    on_tactic: EventHandler<Tactic>,
227) -> Element {
228    let has_proof = !proof_text.is_empty();
229
230    let status_class = match &status {
231        ProofStatus::Idle => "proof-status",
232        ProofStatus::Proving => "proof-status pending",
233        ProofStatus::Success => "proof-status success",
234        ProofStatus::Failed(_) => "proof-status error",
235    };
236
237    let status_text = match &status {
238        ProofStatus::Idle => "Ready to prove".to_string(),
239        ProofStatus::Proving => "Searching for proof...".to_string(),
240        ProofStatus::Success => "\u{2713} Proof found".to_string(),
241        ProofStatus::Failed(msg) => format!("\u{2717} {}", msg),
242    };
243
244    rsx! {
245        style { "{PROOF_PANEL_STYLE}" }
246
247        div { class: "proof-panel",
248            // Status header
249            div { class: "proof-header",
250                span { class: "{status_class}", "{status_text}" }
251            }
252
253            // Tactics bar
254            div { class: "tactics-bar",
255                for tactic in [Tactic::Auto, Tactic::ModusPonens, Tactic::UniversalInst, Tactic::ExistentialIntro, Tactic::Induction, Tactic::Rewrite] {
256                    button {
257                        class: "tactic-btn",
258                        title: "{tactic.description()}",
259                        onclick: {
260                            let t = tactic;
261                            move |_| on_tactic.call(t)
262                        },
263                        "{tactic.label()}"
264                    }
265                }
266            }
267
268            // Derivation tree or empty state
269            if has_proof {
270                div { class: "derivation-tree",
271                    dangerous_inner_html: "{proof_text}",
272                }
273            } else {
274                div { class: "proof-empty",
275                    div { class: "proof-empty-icon", "\u{1F50D}" }
276                    div { class: "proof-empty-text",
277                        "Enter a logical formula to prove."
278                        br {}
279                        "Click 'Auto' to search for a proof automatically,"
280                        br {}
281                        "or use specific tactics to guide the proof."
282                    }
283                }
284            }
285
286            // Socratic hint
287            if let Some(hint_text) = &hint {
288                div { class: "socratic-hint",
289                    span { class: "hint-icon", "\u{1F4A1}" }
290                    span { "{hint_text}" }
291                }
292            }
293        }
294    }
295}