logicaffeine_web/ui/components/
proof_panel.rs1use 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#[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#[derive(Clone, PartialEq, Default)]
208pub enum ProofStatus {
209 #[default]
210 Idle,
211 Proving,
212 Success,
213 Failed(String),
214}
215
216#[component]
218pub fn ProofPanel(
219 proof_text: String,
221 status: ProofStatus,
223 hint: Option<String>,
225 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 div { class: "proof-header",
250 span { class: "{status_class}", "{status_text}" }
251 }
252
253 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 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 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}