1use crate::ui::components::ast_tree::AstTree;
10use crate::ui::components::code_editor::{CodeEditor, CodeView, Language};
11use crate::ui::components::collapsible::Collapsible;
12use crate::ui::components::context_view::ContextView;
13use crate::ui::components::debug_drawer::{DebugDrawer, IC_BUG};
14use crate::ui::components::file_browser::FileBrowser;
15use crate::ui::components::indent::Indentation;
16use crate::ui::components::logic_output::{LogicOutput, OutputFormat};
17use crate::ui::components::proof_panel::{dock_prompt, ProofNode, ProofPanel, ProofStatus, Walkthrough};
18use crate::ui::components::repl_output::ReplOutput;
19use crate::ui::components::socratic_guide::{logic_guide, GuideMode, SocraticGuide};
20use crate::ui::components::status_bar::{EditorStatus, StatusBar};
21use crate::ui::components::symbol_dictionary::{symbols_in, SymbolGroups};
22use crate::ui::examples::{compiled_runtime_notice, planted_bug_notice};
23use crate::ui::fault::{contain, ActionFault};
24use crate::ui::hooks::{use_keyboard_inset, use_media, use_viewport_width};
25use crate::ui::pages::heal;
26use crate::ui::pages::hardware_report::{
27 analyze as analyze_hardware, compiled as compile_hardware, kind_of as hardware_kind_of,
28 Compiled, HardwareKind, HardwareReport,
29};
30use crate::ui::pages::studio::{
31 attempt, auto_view, formula_on_screen, kg_svg, parse_math_statements, run_logic, run_math,
32 traffic_svg, waveform_svg, CodeOutputMode, LogicRun, LogicView, MathContext, TheoremView,
33 COLLAPSE_FILES_SVG, FILES_SVG, STUDIO_STYLE,
34};
35use crate::ui::pages::studio_actions::{actions_for, Actions, Offer};
36use crate::ui::pages::studio_files::example_text;
37use crate::ui::pages::studio_layout::{first_pane, Columns, MobileTab, Resizer, Shown, DRAWER_INITIAL, PROOF_DRAWER_INITIAL};
38use crate::ui::pages::studio_opening::StudioOpening;
39use crate::ui::responsive::media;
40use crate::ui::router::studio_file_url;
41use crate::ui::state::{FileNode, ReplLine, StudioMode};
42use dioxus::prelude::*;
43#[cfg(target_arch = "wasm32")]
44use logicaffeine_compile::interpret_for_ui_baseline;
45use logicaffeine_compile::{
46 extract_logic_rust, extract_math_rust, generate_rust_code, interpret_streaming_with_vfs,
47 interpreter::InterpreterResult, CompileResult, TheoremCompileResult,
48};
49use logicaffeine_kernel::interface::Repl;
50use logicaffeine_language::symbol_dict::SymbolEntry;
51use logicaffeine_proof::ProofExpr;
52use logicaffeine_system::fs::Vfs;
53use std::cell::RefCell;
54use std::collections::HashMap;
55use std::rc::Rc;
56use std::sync::Arc;
57
58pub const PREVIEW_TITLE: &str = "The page, built from the code";
60
61#[derive(Clone, Copy, PartialEq, Eq, Debug)]
63pub enum Host {
64 Page,
67 Frame,
71}
72
73#[derive(Clone, PartialEq, Default)]
76pub struct StoreNote {
77 pub error: Option<String>,
78 pub private_mode: bool,
79 pub new_file_waits: Option<String>,
80}
81
82pub struct Shelf {
84 pub opens: &'static str,
85 pub files: &'static [&'static str],
86}
87
88pub fn use_shelf(shelf: &'static Shelf) -> WorkbenchState {
91 let opening = use_hook(|| StudioOpening::for_route(Some(shelf.opens)));
92 use_workbench(&opening, || FileNode::from_paths(shelf.files.iter().copied()))
93}
94
95pub fn prebuilt_preview_path(vfs_path: &str) -> Option<String> {
100 let text = example_text(vfs_path)?;
101 (StudioMode::for_file(vfs_path, text) == Some(StudioMode::Web))
102 .then(|| format!("/previews{}.html", vfs_path.strip_prefix("/examples").unwrap_or(vfs_path)))
103}
104
105pub const PREVIEW_CENTERING_STYLE: &str = "<style data-studio=\"preview-centering\">html{min-height:100%;display:flex;flex-direction:column;justify-content:safe center}</style>";
110
111pub fn preview_document(source: &str) -> String {
115 match logicaffeine_compile::web_host::assemble_preview_painted(source) {
116 Ok(doc) => match doc.find("</head>") {
117 Some(at) => format!("{}{}{}", &doc[..at], PREVIEW_CENTERING_STYLE, &doc[at..]),
118 None => doc,
119 },
120 Err(pe) => logicaffeine_compile::web_host::error_overlay_html(&pe, source, ""),
121 }
122}
123
124pub const FRAME_ID: &str = "studio-frame";
128
129#[component]
130pub fn StudioFrame(state: WorkbenchState) -> Element {
131 rsx! {
132 div { id: FRAME_ID, class: "studio-frame",
133 style { "{STUDIO_STYLE}" }
134 style { "{crate::ui::drag::DRAG_STYLE}" }
135 style { "{FRAME_STYLE}" }
136 Workbench { state, host: Host::Frame }
137 }
138 }
139}
140
141pub const FRAME_STYLE: &str = r#"
146.studio-frame {
147 position: relative;
148 display: flex;
149 flex-direction: column;
150 width: calc(100% - 32px);
151 max-width: 1600px;
152 height: 720px;
153 margin: 0 auto;
154 background: var(--studio-bg);
155 color: var(--studio-text);
156 border: 1px solid var(--studio-border);
157 border-radius: var(--radius-lg);
158 overflow: hidden;
159 scrollbar-color: var(--studio-border-hover) transparent;
160}
161
162.studio-frame .studio-content {
163 --col-sidebar: 240px;
164 --col-left: 35%;
165 --col-right: 25%;
166}
167
168.studio-frame .studio-sidebar {
169 border-right: 1px solid var(--studio-border);
170}
171
172.studio-frame .studio-main > .col-left,
173.studio-frame .studio-main > .col-right {
174 min-width: 200px;
175}
176
177/* The way into the Studio, at the toolbar's end, in the toolbar's quieter text. */
178.studio-frame .studio-open {
179 display: inline-flex;
180 align-items: center;
181 min-height: 32px;
182 padding: 0 12px;
183 border: 1px solid var(--accent-primary);
184 border-radius: 6px;
185 color: var(--studio-text);
186 font-size: 13px;
187 font-weight: 600;
188 text-decoration: none;
189 white-space: nowrap;
190}
191
192.studio-frame .studio-open:hover {
193 background: var(--accent-primary);
194 color: #fff;
195}
196
197@media (min-width: 720px) {
198 .studio-frame {
199 width: calc(100% - 48px);
200 }
201}
202
203/* The way into the Studio is a finger wide wherever a finger may press it. */
204@media (max-width: 768px), (pointer: coarse), (max-height: 500px) and (orientation: landscape) {
205 .studio-frame .studio-open {
206 min-height: 44px;
207 }
208}
209
210/* The frame's height in each band, set by the stylesheet alone, so the server's render is the first
211 frame at every width: some page always shows above or below it on a phone, where a swipe scrolls
212 the page. */
213@media (min-width: 769px) and (min-height: 501px) and (pointer: coarse) {
214 .studio-frame {
215 height: min(720px, 85svh);
216 }
217}
218
219@media (min-width: 601px) and (max-width: 768px), (min-width: 769px) and (max-width: 1024px) and (orientation: portrait) and (pointer: coarse) {
220 .studio-frame {
221 height: min(960px, 85svh);
222 }
223}
224
225@media (max-width: 600px) {
226 .studio-frame {
227 height: min(640px, 75svh);
228 }
229}
230
231@media (max-height: 500px) and (orientation: landscape) {
232 .studio-frame {
233 height: 85svh;
234 }
235}
236"#;
237
238#[derive(Clone, Copy, PartialEq)]
243pub struct WorkbenchState {
244 pub(super) mode: Signal<StudioMode>,
247 pub(super) sidebar_open: Signal<bool>,
249 pub(super) drawer: Signal<bool>,
251 pub(super) drawer_open: Signal<bool>,
253 pub(super) panes_fixed: Signal<bool>,
256 pub(super) keyboard_inset: Signal<f64>,
258 pub(super) file_tree: Signal<FileNode>,
260 pub(super) current_file: Signal<Option<String>>,
262 pub(super) edits: Signal<HashMap<String, String>>,
265 pub(super) input: Signal<String>,
267 pub(super) result: Signal<CompileResult>,
268 pub(super) format: Signal<OutputFormat>,
269 pub(super) proof_text: Signal<String>,
270 pub(super) proof_tree: Signal<Option<ProofNode>>,
272 pub(super) proof_status: Signal<ProofStatus>,
273 pub(super) proof_hint: Signal<Option<String>>,
274 pub(super) current_proof_expr: Signal<Option<ProofExpr>>,
275 pub(super) knowledge_base: Signal<Vec<ProofExpr>>,
276 pub(super) logic_output_mode: Signal<LogicView>,
277 pub(super) generated_logic_rust: Signal<String>,
278 pub(super) dictionary_open: Signal<bool>,
279 pub(super) tree_open: Signal<bool>,
280 pub(super) dictionary: Memo<Option<Vec<SymbolEntry>>>,
281 pub(super) proof_height: Signal<f64>,
283 pub(super) proof_open: Signal<bool>,
284 pub(super) code_input: Signal<String>,
287 pub(super) code_output_mode: Signal<CodeOutputMode>,
288 pub(super) interpreter_result: Signal<InterpreterResult>,
289 pub(super) generated_rust: Signal<String>,
290 pub(super) debugging: Signal<bool>,
291 pub(super) debug_output: Signal<InterpreterResult>,
292 pub(super) drawer_height: Signal<f64>,
293 pub(super) program_store: Signal<Option<Arc<dyn Vfs>>>,
295 pub(super) math_input: Signal<String>,
297 pub(super) math_context: Signal<MathContext>,
298 pub(super) math_output: Signal<Vec<ReplLine>>,
299 pub(super) math_output_mode: Signal<CodeOutputMode>,
300 pub(super) generated_math_rust: Signal<String>,
301 pub(super) hw_input: Signal<String>,
303 pub(super) hw_output_mode: Signal<CodeOutputMode>,
304 pub(super) hw_report: Signal<Option<(HardwareKind, HardwareReport)>>,
305 pub(super) generated_hw_rust: Signal<String>,
306 pub(super) hw_compiled: Signal<Option<Compiled>>,
307 pub(super) debounced_code: Signal<String>,
309 pub(super) web_edit_gen: Signal<u64>,
310 pub(super) web_preview: Memo<String>,
311 pub(super) fault: Signal<Option<ActionFault>>,
313 pub(super) actions: Memo<Actions>,
314 pub(super) editor_status: Signal<EditorStatus>,
315 pub(super) heal: Signal<heal::Flight>,
317 pub(super) heal_baseline: Signal<std::collections::HashMap<String, String>>,
320 pub(super) column_pref: Signal<Option<Columns>>,
322 pub(super) viewport_width: Signal<f64>,
323 pub(super) active_tab: Signal<MobileTab>,
324 pub(super) editor_expanded: Signal<bool>,
325 pub(super) output_expanded: Signal<bool>,
326 pub(super) mobile_split: Signal<f64>,
327}
328
329pub fn use_workbench(opening: &StudioOpening, tree: impl FnOnce() -> FileNode) -> WorkbenchState {
332 let mode = use_signal(|| opening.mode);
334
335 let sidebar_open = use_signal(|| true);
337 let drawer = use_media(media::FILES_DRAWER);
338 let drawer_open = use_signal(|| false);
339 let panes_fixed = use_media(media::PANES_FIXED);
340 let keyboard_inset = use_keyboard_inset();
341 let file_tree = use_signal(tree);
344 let current_file = use_signal(|| opening.current_file());
345 let edits = use_signal(HashMap::new);
346
347 let input = use_signal(|| opening.editor_text(StudioMode::Logic));
349 let result = use_signal(|| CompileResult {
350 logic: None,
351 simple_logic: None,
352 kripke_logic: None,
353 ast: None,
354 readings: Vec::new(),
355 simple_readings: Vec::new(),
356 kripke_readings: Vec::new(),
357 tokens: Vec::new(),
358 error: None,
359 });
360 let format = use_signal(|| OutputFormat::SimpleFOL);
361
362 let proof_text = use_signal(String::new);
364 let proof_tree = use_signal(|| None::<ProofNode>);
365 let proof_status = use_signal(|| ProofStatus::Idle);
366 let proof_hint = use_signal(|| None::<String>);
367 let current_proof_expr = use_signal(|| None::<ProofExpr>);
369 let knowledge_base = use_signal(Vec::<ProofExpr>::new);
371 let logic_output_mode = use_signal(|| LogicView::Logic);
373 let generated_logic_rust = use_signal(String::new);
374 let dictionary_open = use_signal(|| true);
377 let tree_open = use_signal(|| false);
378 let dictionary = use_memo(move || {
380 formula_on_screen(&result.read(), *format.read())
381 .map(|(logic, view)| symbols_in(&logic, view))
382 .filter(|symbols| !symbols.is_empty())
383 });
384
385 let code_input = use_signal(|| opening.editor_text(StudioMode::Code));
387 let code_output_mode = use_signal(|| CodeOutputMode::Interpret);
388 let interpreter_result = use_signal(|| InterpreterResult {
389 lines: vec![],
390 error: None,
391 });
392 let generated_rust = use_signal(String::new);
393 let debugging = use_signal(|| false);
395
396 let math_input = use_signal(|| opening.editor_text(StudioMode::Math));
398 let math_context = use_signal(MathContext::prelude);
400 let math_output = use_signal(Vec::<ReplLine>::new);
401 let math_output_mode = use_signal(|| CodeOutputMode::Interpret);
403 let generated_math_rust = use_signal(String::new);
404
405 let hw_input = use_signal(|| opening.editor_text(StudioMode::Hardware));
407 let hw_output_mode = use_signal(|| CodeOutputMode::Interpret);
409 let mut hw_report = use_signal(|| None::<(HardwareKind, HardwareReport)>);
412 let generated_hw_rust = use_signal(String::new);
413 let hw_compiled = use_signal(|| None::<Compiled>);
416
417 let fault = use_signal(|| None::<ActionFault>);
420
421 let editor = move |mode: StudioMode| match mode {
423 StudioMode::Logic => input,
424 StudioMode::Code | StudioMode::Web => code_input,
425 StudioMode::Math => math_input,
426 StudioMode::Hardware => hw_input,
427 };
428
429 let actions = use_memo(move || {
433 let mode = *mode.read();
434 let path = current_file.read().clone();
435 let content = editor(mode).read().clone();
436 contain(|| actions_for(mode, path.as_deref(), &content)).unwrap_or(Actions {
437 run: Offer::Ready,
438 compile: Offer::Ready,
439 debug: Offer::Ready,
440 })
441 });
442
443 let column_pref = use_signal(|| None::<Columns>);
446 let viewport_width = use_viewport_width();
447 let drawer_height = use_signal(|| DRAWER_INITIAL);
449 let debug_output = use_signal(|| InterpreterResult {
452 lines: Vec::new(),
453 error: None,
454 });
455 let editor_status = use_signal(EditorStatus::default);
457 let proof_height = use_signal(|| PROOF_DRAWER_INITIAL);
458 let proof_open = use_signal(|| true);
459 let active_tab = use_signal(|| first_pane(opening.mode));
461
462 let editor_expanded = use_signal(|| true);
464 let output_expanded = use_signal(|| true);
465
466 let mobile_split = use_signal(|| 50.0f64);
468
469 let program_store = use_signal(|| None::<Arc<dyn Vfs>>);
472
473 let mut debounced_code = use_signal(|| {
480 if opening.mode == StudioMode::Web {
481 opening.editor_text(StudioMode::Web)
482 } else {
483 String::new()
484 }
485 });
486 let mut web_edit_gen = use_signal(|| 0u64);
487 let mut settled_file = use_signal(|| opening.current_file());
490 use_effect(move || {
491 if *mode.read() != StudioMode::Web {
492 return;
493 }
494 let src = code_input.read().clone();
495 let file = current_file.read().clone();
496 if *debounced_code.peek() == src {
497 return;
498 }
499 if debounced_code.peek().is_empty() || *settled_file.peek() != file {
502 settled_file.set(file);
503 debounced_code.set(src);
504 return;
505 }
506 let my_gen = *web_edit_gen.peek() + 1;
507 web_edit_gen.set(my_gen);
508 spawn(async move {
509 gloo_timers::future::TimeoutFuture::new(300).await;
510 if *web_edit_gen.peek() == my_gen {
511 debounced_code.set(src);
512 }
513 });
514 });
515
516 let built = use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(std::collections::HashMap::<String, String>::new())));
523 let prebuilt = use_signal(std::collections::HashMap::<String, String>::new);
529 let last_doc = use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(String::new())));
530 #[cfg(target_arch = "wasm32")]
538 let hydrating = use_hook(|| {
539 let served = web_sys::window()
540 .and_then(|w| w.document())
541 .and_then(|d| d.query_selector(&format!("iframe[title=\"{PREVIEW_TITLE}\"]")).ok().flatten())
542 .and_then(|frame| frame.get_attribute("srcdoc"))
543 .filter(|d| !d.is_empty());
544 let Some(doc) = served else { return false };
545 if opening.mode == StudioMode::Web {
546 built.borrow_mut().insert(opening.editor_text(StudioMode::Web), doc.clone());
547 *last_doc.borrow_mut() = doc;
548 }
549 true
550 });
551 #[cfg(target_arch = "wasm32")]
552 {
553 let in_flight = use_signal(std::collections::HashSet::<String>::new);
554 let fetch = move |path: String, url: String| {
556 let mut in_flight = in_flight;
557 let mut prebuilt = prebuilt;
558 if prebuilt.peek().contains_key(&path) || in_flight.peek().contains(&path) {
559 return;
560 }
561 in_flight.write().insert(path.clone());
562 spawn(async move {
563 if let Ok(response) = gloo_net::http::Request::get(&url).send().await {
564 if response.ok() {
565 if let Ok(doc) = response.text().await {
566 prebuilt.write().insert(path.clone(), doc);
567 }
568 }
569 }
570 in_flight.write().remove(&path);
571 });
572 };
573 use_effect(move || {
576 if *mode.read() != StudioMode::Web {
577 return;
578 }
579 let Some(path) = current_file.read().clone() else { return };
580 let Some(url) = prebuilt_preview_path(&path) else { return };
581 fetch(path, url);
582 });
583 use_hook(|| {
584 spawn(async move {
585 gloo_timers::future::TimeoutFuture::new(1500).await;
586 for spec in crate::ui::examples::seeded_examples() {
587 if let Some(url) = prebuilt_preview_path(spec.vfs_path) {
588 fetch(spec.vfs_path.to_string(), url);
589 }
590 }
591 });
592 });
593 }
594 let web_preview = use_memo(move || {
595 if *mode.read() != StudioMode::Web {
596 return String::new();
597 }
598 let src = debounced_code.read().clone();
599 if let Some(doc) = built.borrow().get(&src) {
600 return doc.clone();
601 }
602 if let Some(path) = current_file.read().as_deref() {
603 if example_text(path).is_some_and(|text| text == src) && prebuilt_preview_path(path).is_some() {
604 if let Some(doc) = prebuilt.read().get(path) {
605 built.borrow_mut().insert(src, doc.clone());
606 *last_doc.borrow_mut() = doc.clone();
607 return doc.clone();
608 }
609 #[cfg(target_arch = "wasm32")]
613 {
614 let last = last_doc.borrow().clone();
615 if !last.is_empty() || !hydrating {
616 return last;
617 }
618 }
619 }
620 }
621 let doc = preview_document(&src);
622 built.borrow_mut().insert(src, doc.clone());
623 *last_doc.borrow_mut() = doc.clone();
624 doc
625 });
626
627 let heal = use_signal(|| heal::Flight::Idle);
628 let heal_baseline = use_signal(|| {
629 let mut baselines = std::collections::HashMap::new();
630 if let Some(path) = opening.current_file() {
631 baselines.insert(path, opening.editor_text(opening.mode));
632 }
633 baselines
634 });
635
636 let state = WorkbenchState {
637 mode,
638 sidebar_open,
639 drawer,
640 drawer_open,
641 panes_fixed,
642 keyboard_inset,
643 file_tree,
644 current_file,
645 edits,
646 input,
647 result,
648 format,
649 proof_text,
650 proof_tree,
651 proof_status,
652 proof_hint,
653 current_proof_expr,
654 knowledge_base,
655 logic_output_mode,
656 generated_logic_rust,
657 dictionary_open,
658 tree_open,
659 dictionary,
660 proof_height,
661 proof_open,
662 code_input,
663 code_output_mode,
664 interpreter_result,
665 generated_rust,
666 debugging,
667 debug_output,
668 drawer_height,
669 program_store,
670 math_input,
671 math_context,
672 math_output,
673 math_output_mode,
674 generated_math_rust,
675 hw_input,
676 hw_output_mode,
677 hw_report,
678 generated_hw_rust,
679 hw_compiled,
680 debounced_code,
681 web_edit_gen,
682 web_preview,
683 fault,
684 actions,
685 editor_status,
686 heal,
687 heal_baseline,
688 column_pref,
689 viewport_width,
690 active_tab,
691 editor_expanded,
692 output_expanded,
693 mobile_split,
694 };
695
696 use_effect(move || {
699 let content = hw_input.read().clone();
700 if matches!(
701 hardware_kind_of(&content),
702 Some(HardwareKind::Pigeonhole | HardwareKind::RegisterAlloc)
703 ) {
704 hw_report.set(analyze_hardware(&content));
705 }
706 });
707
708 #[cfg(target_arch = "wasm32")]
711 {
712 let opening = opening.clone();
713 let mut started = use_signal(|| false);
714 use_effect(move || {
715 if *started.read() {
716 return;
717 }
718 started.set(true);
719 if let Some(text) = opening.text {
720 let (opened_mode, path) = (opening.mode, opening.path.clone());
721 spawn(async move {
722 gloo_timers::future::TimeoutFuture::new(0).await;
724 state.run_opened(opened_mode, path, text.to_string()).await;
725 });
726 }
727 });
728 }
729
730 state
731}
732
733impl WorkbenchState {
734 pub fn editor(self, mode: StudioMode) -> Signal<String> {
736 match mode {
737 StudioMode::Logic => self.input,
738 StudioMode::Code | StudioMode::Web => self.code_input,
739 StudioMode::Math => self.math_input,
740 StudioMode::Hardware => self.hw_input,
741 }
742 }
743
744 pub(super) fn show_theorem(self, theorem: Result<(TheoremCompileResult, TheoremView), String>) {
747 let WorkbenchState { mut result, mut proof_text, mut proof_tree, mut proof_status, mut proof_hint, mut current_proof_expr, mut knowledge_base, .. } = self;
748 match theorem {
749 Err(err) => {
750 result.set(CompileResult {
751 logic: None,
752 simple_logic: None,
753 kripke_logic: None,
754 ast: None,
755 readings: Vec::new(),
756 simple_readings: Vec::new(),
757 kripke_readings: Vec::new(),
758 tokens: Vec::new(),
759 error: Some(err.clone()),
760 });
761 proof_status.set(ProofStatus::Failed(err));
762 current_proof_expr.set(None);
763 knowledge_base.write().clear();
764 }
765 Ok((theorem, view)) => {
766 result.set(CompileResult {
767 logic: theorem.goal_string.clone(),
768 simple_logic: theorem.goal_string.clone(),
769 kripke_logic: None,
770 ast: None,
771 readings: Vec::new(),
772 simple_readings: Vec::new(),
773 kripke_readings: Vec::new(),
774 tokens: Vec::new(),
775 error: None,
776 });
777 knowledge_base.set(theorem.premises);
778 if let Some(goal) = theorem.goal {
779 current_proof_expr.set(Some(goal));
780 }
781 proof_text.set(view.html);
782 proof_tree.set(view.tree);
783 proof_status.set(view.status);
784 proof_hint.set(Some(view.hint));
785 }
786 }
787 }
788
789 #[cfg(target_arch = "wasm32")]
793 async fn run_opened(self, file_mode: StudioMode, path: String, content: String) {
794 let WorkbenchState { mut result, mut current_proof_expr, mut interpreter_result, mut hw_report, mut hw_compiled, fault, .. } = self;
795 match file_mode {
796 StudioMode::Logic => match attempt(fault, "Execute", || run_logic(&content)) {
797 Some(LogicRun::Theorem(theorem)) => self.show_theorem(theorem),
798 Some(LogicRun::Sentences(compiled, proof)) => {
799 result.set(compiled);
800 if let Some(expr) = proof.proof_expr {
801 current_proof_expr.set(Some(expr));
802 }
803 }
804 Some(LogicRun::Empty) | None => {}
805 },
806 StudioMode::Code => {
808 interpreter_result.set(if compiled_runtime_notice(&path).is_some() {
809 InterpreterResult {
810 lines: Vec::new(),
811 error: None,
812 }
813 } else {
814 interpret_for_ui_baseline(&content).await
815 })
816 }
817 StudioMode::Hardware => {
818 hw_report.set(analyze_hardware(&content));
819 hw_compiled.set(None);
820 }
821 StudioMode::Math | StudioMode::Web => {}
822 }
823 }
824
825 #[cfg(target_arch = "wasm32")]
830 pub async fn open(self, path: String, content: String) {
831 let WorkbenchState { mut mode, mut fault, .. } = self;
832 let file_mode = StudioMode::for_file(&path, &content).unwrap_or(*mode.peek());
833 mode.set(file_mode);
834 if fault.peek().is_some() {
836 fault.set(None);
837 }
838 let content = self.opening_text(&path, content);
839 self.editor(file_mode).set(content.clone());
840 let mut baseline = self.heal_baseline;
842 baseline.write().insert(path.clone(), content.clone());
843 self.run_opened(file_mode, path, content).await;
844 }
845
846 #[cfg(any(target_arch = "wasm32", test))]
848 pub(super) fn opening_text(self, path: &str, content: String) -> String {
849 self.edits.peek().get(path).cloned().unwrap_or(content)
850 }
851
852 fn keep_edit(self) {
854 let WorkbenchState { mode, current_file, mut edits, .. } = self;
855 let open = current_file.peek().clone();
856 if let Some(path) = open {
857 let text = self.editor(*mode.peek()).peek().clone();
858 edits.write().insert(path, text);
859 }
860 }
861
862 pub fn choose(self, path: String) {
866 let mut current_file = self.current_file;
867 let mut drawer_open = self.drawer_open;
868 if *self.drawer.peek() && *drawer_open.peek() {
869 drawer_open.set(false);
870 }
871 self.keep_edit();
872 current_file.set(Some(path.clone()));
873 #[cfg(target_arch = "wasm32")]
874 if let Some(text) = example_text(&path) {
875 let mut active_tab = self.active_tab;
876 let file_mode = StudioMode::for_file(&path, text).unwrap_or(*self.mode.peek());
877 spawn(async move {
878 active_tab.set(first_pane(file_mode));
879 self.open(path, text.to_string()).await;
880 });
881 }
882 }
883}
884
885#[cfg(target_arch = "wasm32")]
889fn pressed(evt: &PointerEvent, selector: &str) -> Option<web_sys::Element> {
890 use wasm_bindgen::JsCast;
891 let target = evt.data().downcast::<web_sys::PointerEvent>()?.target()?;
892 target.dyn_into::<web_sys::Element>().ok()?.closest(selector).ok().flatten()
893}
894
895#[component]
897pub fn Workbench(
898 state: WorkbenchState,
899 host: Host,
900 #[props(default)]
902 store: StoreNote,
903 #[props(default)]
906 on_new_file: Option<EventHandler<()>>,
907 #[props(default)]
910 on_choose: Option<EventHandler<String>>,
911) -> Element {
912 let WorkbenchState { mode, mut sidebar_open, drawer, mut drawer_open, panes_fixed, keyboard_inset, mut file_tree, current_file, mut input, mut result, mut format, mut proof_text, mut proof_tree, mut proof_status, mut proof_hint, mut current_proof_expr, mut knowledge_base, mut logic_output_mode, mut generated_logic_rust, dictionary_open, tree_open, dictionary, proof_height, mut proof_open, mut code_input, mut code_output_mode, mut interpreter_result, mut generated_rust, mut debugging, debug_output, drawer_height, program_store, mut math_input, mut math_context, mut math_output, mut math_output_mode, mut generated_math_rust, mut hw_input, mut hw_output_mode, mut hw_report, mut generated_hw_rust, mut hw_compiled, web_preview, fault, actions, editor_status, mut heal, mut heal_baseline, column_pref, viewport_width, mut active_tab, mut editor_expanded, mut output_expanded, mobile_split, .. } = state;
913
914 let mut frame_row_width = use_signal(|| None::<f64>);
917 use_effect(move || {
918 let _ = viewport_width.read();
919 if host != Host::Frame {
920 return;
921 }
922 #[cfg(target_arch = "wasm32")]
923 {
924 let measured = web_sys::window()
925 .and_then(|w| w.document())
926 .and_then(|d| d.query_selector(".studio-frame .studio-content").ok().flatten())
927 .map(|row| row.client_width() as f64)
928 .filter(|w| *w > 0.0);
929 if measured.is_some() {
930 frame_row_width.set(measured);
931 }
932 }
933 #[cfg(not(target_arch = "wasm32"))]
934 {
935 let _ = &mut frame_row_width;
936 }
937 });
938 let show_theorem = move |theorem| state.show_theorem(theorem);
939 let mut fault_open = use_signal(|| false);
941
942
943 let handle_logic_input = move |new_value: String| {
948 input.set(new_value);
949 };
950
951 let handle_code_run = move |_| {
953 let code = code_input.read().clone();
954 active_tab.set(MobileTab::Panel2);
956 code_output_mode.set(CodeOutputMode::Interpret);
957 interpreter_result.set(InterpreterResult {
959 lines: vec![],
960 error: None,
961 });
962 spawn(async move {
963 let callback = Rc::new(RefCell::new(move |line: String| {
965 interpreter_result.write().lines.push(line);
967 }));
968
969 let vfs = program_store.read().clone();
973 let result = interpret_streaming_with_vfs(&code, callback, vfs).await;
974 interpreter_result.set(result);
976 });
977 };
978
979 let handle_code_compile = move |_| {
982 let code = code_input.read().clone();
983 active_tab.set(MobileTab::Panel2);
985 spawn(async move {
986 #[cfg(target_arch = "wasm32")]
988 gloo_timers::future::TimeoutFuture::new(0).await;
989 match generate_rust_code(&code) {
990 Ok(rust_code) => {
991 generated_rust.set(rust_code);
992 code_output_mode.set(CodeOutputMode::Rust);
993 }
994 Err(e) => {
995 interpreter_result.set(InterpreterResult {
996 lines: vec![],
997 error: Some(format!("Compile error: {:?}", e)),
998 });
999 code_output_mode.set(CodeOutputMode::Interpret);
1000 }
1001 }
1002 });
1003 };
1004
1005 let handle_math_execute = move |_| {
1011 active_tab.set(MobileTab::Panel2);
1012 math_output_mode.set(CodeOutputMode::Interpret);
1013 let code = math_input.read().clone();
1014 spawn(async move {
1015 #[cfg(target_arch = "wasm32")]
1017 gloo_timers::future::TimeoutFuture::new(0).await;
1018 let Some(run) = attempt(fault, "Execute", || run_math(&code)) else {
1019 return;
1020 };
1021 if let Some(context) = run.context {
1022 if *math_context.peek() != context {
1023 math_context.set(context);
1024 }
1025 }
1026 math_output.set(run.lines);
1027 });
1028 };
1029
1030 let handle_math_compile = move |_| {
1033 active_tab.set(MobileTab::Panel2);
1034 let code = math_input.read().clone();
1035 spawn(async move {
1036 #[cfg(target_arch = "wasm32")]
1038 gloo_timers::future::TimeoutFuture::new(0).await;
1039 let compiled = attempt(fault, "Compile", || {
1040 let mut repl = Repl::new();
1041 for stmt in parse_math_statements(&code) {
1042 let _ = repl.execute(&stmt);
1043 }
1044 let rust = match extract_math_rust(repl.context()) {
1045 Ok(rust) => rust,
1046 Err(e) => format!("// extraction error: {e}"),
1047 };
1048 (MathContext::of(repl.context()), rust)
1049 });
1050 let Some((context, rust)) = compiled else {
1051 return;
1052 };
1053 if *math_context.peek() != context {
1054 math_context.set(context);
1055 }
1056 generated_math_rust.set(rust);
1057 math_output_mode.set(CodeOutputMode::Rust);
1058 });
1059 };
1060
1061 let handle_logic_execute = move |_| {
1065 active_tab.set(MobileTab::Panel2);
1066 logic_output_mode.set(LogicView::Logic);
1067 let text = input.read().clone();
1068 match attempt(fault, "Execute", || run_logic(&text)) {
1069 Some(LogicRun::Theorem(theorem)) => show_theorem(theorem),
1070 Some(LogicRun::Sentences(compiled, proof)) => {
1071 result.set(compiled);
1072 knowledge_base.write().clear();
1073 if let Some(expr) = proof.proof_expr {
1074 current_proof_expr.set(Some(expr));
1075 proof_status.set(ProofStatus::Idle);
1076 proof_hint.set(Some("Press Prove to check the first sentence, or give it Given lines in a ## Theorem: block.".to_string()));
1077 } else if let Some(err) = proof.error {
1078 current_proof_expr.set(None);
1079 proof_status.set(ProofStatus::Failed(err));
1080 }
1081 }
1082 Some(LogicRun::Empty) => {
1083 result.set(CompileResult {
1084 logic: None,
1085 simple_logic: None,
1086 kripke_logic: None,
1087 ast: None,
1088 readings: Vec::new(),
1089 simple_readings: Vec::new(),
1090 kripke_readings: Vec::new(),
1091 tokens: Vec::new(),
1092 error: None,
1093 });
1094 current_proof_expr.set(None);
1095 knowledge_base.write().clear();
1096 proof_text.set(String::new());
1097 proof_tree.set(None);
1098 proof_status.set(ProofStatus::Idle);
1099 }
1100 None => {}
1101 }
1102 };
1103
1104 let handle_logic_compile = move |_| {
1107 active_tab.set(MobileTab::Panel2);
1108 let text = input.read().clone();
1109 spawn(async move {
1110 #[cfg(target_arch = "wasm32")]
1112 gloo_timers::future::TimeoutFuture::new(0).await;
1113 let extracted = attempt(fault, "Compile", || match extract_logic_rust(&text) {
1114 Ok(rust) => rust,
1115 Err(e) => format!("// extraction error: {e}"),
1116 });
1117 let Some(rust) = extracted else { return };
1118 generated_logic_rust.set(rust);
1119 logic_output_mode.set(LogicView::Rust);
1120 });
1121 };
1122
1123 let handle_prove = use_callback(move |_: ()| {
1127 let maybe_goal = current_proof_expr.read().clone();
1128 let kb = knowledge_base.read().clone();
1129 let logic_str = result.read().simple_logic.as_deref().and_then(|all| all.lines().next()).map(|first| {
1132 first.trim_start_matches(|c: char| c.is_ascii_digit() || c == ')' || c == ' ').to_string()
1133 });
1134 proof_open.set(true);
1135 match maybe_goal {
1136 Some(goal) => {
1137 let before = proof_status.peek().clone();
1138 proof_status.set(ProofStatus::Proving);
1139 let shown = attempt(fault, "Prove", || auto_view(&kb, &goal, logic_str.as_deref().unwrap_or("(no expression)")));
1140 match shown {
1141 Some(view) => {
1142 let hint = if kb.is_empty() && matches!(view.status, ProofStatus::Failed(_)) {
1145 "No givens to prove from. Add Given lines in a ## Theorem: block, as prover-demo.logic does.".to_string()
1146 } else {
1147 view.hint
1148 };
1149 proof_text.set(view.html);
1150 proof_tree.set(view.tree);
1151 proof_status.set(view.status);
1152 proof_hint.set(Some(hint));
1153 }
1154 None => proof_status.set(before),
1156 }
1157 }
1158 None => {
1159 proof_status.set(ProofStatus::Failed("No logic expression to prove. Enter a sentence first.".to_string()));
1160 proof_text.set(String::new());
1161 proof_tree.set(None);
1162 proof_hint.set(Some("Enter an English sentence above to generate a logical formula.".to_string()));
1163 }
1164 }
1165 });
1166
1167 let handle_hardware_execute = move |_| {
1170 hw_report.set(analyze_hardware(&hw_input.read()));
1171 hw_compiled.set(None);
1172 hw_output_mode.set(CodeOutputMode::Interpret);
1173 };
1174
1175 let handle_hardware_compile = move |_| match compile_hardware(&hw_input.read()) {
1176 Compiled::Rust(rust) => {
1177 generated_hw_rust.set(rust);
1178 hw_compiled.set(None);
1179 hw_output_mode.set(CodeOutputMode::Rust);
1180 }
1181 said => {
1182 generated_hw_rust.set(String::new());
1183 hw_compiled.set(Some(said));
1184 hw_output_mode.set(CodeOutputMode::Interpret);
1185 }
1186 };
1187
1188 let current_result = result.read();
1190 let guide_mode = if *mode.read() == StudioMode::Logic {
1191 logic_guide(
1192 current_result.error.as_deref(),
1193 current_result.logic.as_deref(),
1194 current_result.readings.len(),
1195 &input.read(),
1196 &proof_status.read(),
1197 )
1198 } else {
1199 GuideMode::Idle
1200 };
1201
1202 let hw_kind = hw_report.read().as_ref().map(|(kind, _)| *kind);
1205 let show_panel3 = match *mode.read() {
1206 StudioMode::Code => interpreter_result.read().error.is_some(),
1207 StudioMode::Math => !math_context.read().is_empty(),
1208 StudioMode::Hardware => match hw_report.read().as_ref() {
1209 Some((HardwareKind::Pigeonhole | HardwareKind::RegisterAlloc, report)) => {
1210 report.error.is_none()
1211 }
1212 Some((_, report)) => {
1213 !report.signals.is_empty()
1214 || !report.trace.is_empty()
1215 || !report.kg.nodes.is_empty()
1216 }
1217 None => false,
1218 },
1219 StudioMode::Logic | StudioMode::Web => false,
1222 };
1223
1224 let shown = Shown {
1225 sidebar: *sidebar_open.read() && !*drawer.read(),
1226 right: show_panel3,
1227 };
1228 let viewport = match (host, *frame_row_width.read()) {
1231 (Host::Frame, Some(width)) => width,
1232 _ => *viewport_width.read(),
1233 };
1234 let columns = column_pref
1235 .read()
1236 .unwrap_or_else(|| Columns::initial(viewport, shown))
1237 .fit(viewport, shown);
1238 let columns_style = (host == Host::Page || column_pref.read().is_some()).then(|| {
1242 format!(
1243 "--col-sidebar: {}px; --col-left: {}px; --col-right: {}px;",
1244 columns.sidebar, columns.left, columns.right
1245 )
1246 });
1247
1248 let start_column_drag = move |evt: PointerEvent, resizer: Resizer| {
1252 #[cfg(target_arch = "wasm32")]
1253 {
1254 use dioxus::html::input_data::MouseButton;
1255 use wasm_bindgen::JsCast;
1256 let mut column_pref = column_pref;
1257 if evt
1258 .data()
1259 .trigger_button()
1260 .is_some_and(|b| b != MouseButton::Primary)
1261 {
1262 return;
1263 }
1264 evt.prevent_default();
1265 let Some(document) = web_sys::window().and_then(|w| w.document()) else {
1266 return;
1267 };
1268 let find = |selector: &str| document.query_selector(selector).ok().flatten();
1269 let Some(handle) = pressed(&evt, "[data-resizer]")
1270 .or_else(|| find(&format!("[data-resizer=\"{}\"]", resizer.attr())))
1271 else {
1272 return;
1273 };
1274 let Some(row) = handle
1275 .closest(".studio-content")
1276 .ok()
1277 .flatten()
1278 .and_then(|el| el.dyn_into::<web_sys::HtmlElement>().ok())
1279 else {
1280 return;
1281 };
1282 let point = evt.data().client_coordinates();
1283 let _ = handle.class_list().add_1("active");
1284 let released = handle.clone();
1285 crate::ui::drag::begin(
1286 handle,
1287 evt.data().pointer_id(),
1288 (point.x, point.y),
1289 crate::ui::drag::Axis::Columns,
1290 move |dx, _| {
1291 let c = columns.dragged(resizer, dx, viewport, shown);
1292 let style = row.style();
1293 let _ = style.set_property("--col-sidebar", &format!("{}px", c.sidebar));
1294 let _ = style.set_property("--col-left", &format!("{}px", c.left));
1295 let _ = style.set_property("--col-right", &format!("{}px", c.right));
1296 },
1297 move |dx, _| {
1298 let _ = released.class_list().remove_1("active");
1299 column_pref.set(Some(columns.dragged(resizer, dx, viewport, shown)));
1300 },
1301 );
1302 }
1303 #[cfg(not(target_arch = "wasm32"))]
1304 let _ = (evt, resizer);
1305 };
1306
1307 let start_split_drag = move |evt: PointerEvent| {
1309 #[cfg(target_arch = "wasm32")]
1310 {
1311 use crate::ui::pages::studio_layout::split_percent;
1312 use wasm_bindgen::JsCast;
1313 let mut mobile_split = mobile_split;
1314 evt.prevent_default();
1315 let Some(document) = web_sys::window().and_then(|w| w.document()) else {
1316 return;
1317 };
1318 let find = |selector: &str| document.query_selector(selector).ok().flatten();
1319 let handle = pressed(&evt, ".mobile-panel-resizer").or_else(|| find(".mobile-panel-resizer"));
1320 let stack = handle.as_ref().and_then(|handle| handle.closest(".studio-main").ok().flatten());
1321 let panel = |selector: &str| {
1322 stack
1323 .as_ref()
1324 .and_then(|stack| stack.query_selector(selector).ok().flatten())
1325 .and_then(|el| el.dyn_into::<web_sys::HtmlElement>().ok())
1326 };
1327 let (Some(handle), Some(upper), Some(lower)) =
1328 (handle, panel(":scope > .col-left"), panel(":scope > .col-center"))
1329 else {
1330 return;
1331 };
1332 let Some(stack) = stack else { return };
1333 let rect = stack.get_bounding_client_rect();
1334 let (top, height) = (rect.top(), rect.height());
1335 let point = evt.data().client_coordinates();
1336 let _ = handle.class_list().add_1("active");
1337 let released = handle.clone();
1338 crate::ui::drag::begin(
1339 handle,
1340 evt.data().pointer_id(),
1341 (point.x, point.y),
1342 crate::ui::drag::Axis::Rows,
1343 move |_, dy| {
1344 let pct = split_percent(point.y + dy, top, height);
1345 let _ = upper
1346 .style()
1347 .set_property("--panel-flex", &format!("{}", pct / 50.0));
1348 let _ = lower
1349 .style()
1350 .set_property("--panel-flex", &format!("{}", (100.0 - pct) / 50.0));
1351 },
1352 move |_, dy| {
1353 let _ = released.class_list().remove_1("active");
1354 mobile_split.set(split_percent(point.y + dy, top, height));
1355 },
1356 );
1357 }
1358 #[cfg(not(target_arch = "wasm32"))]
1359 let _ = evt;
1360 };
1361
1362 let current_format = *format.read();
1363 let current_mode = *mode.read();
1364 let current_tab = match *active_tab.read() {
1367 MobileTab::Panel3 if !show_panel3 => MobileTab::Panel2,
1368 tab => tab,
1369 };
1370 let on_phone = |tab: MobileTab| if current_tab == tab { " phone-shown" } else { "" };
1371 let right_half = if current_tab == MobileTab::Panel3 { MobileTab::Panel3 } else { MobileTab::Panel2 };
1374 let on_right = |tab: MobileTab| if right_half == tab { " phone-right" } else { "" };
1375
1376 let editor_exp = *editor_expanded.read();
1379 let output_exp = *output_expanded.read();
1380 let panel1_class = format!("studio-panel {}{}", if editor_exp { "mobile-expanded" } else { "mobile-collapsed" }, on_phone(MobileTab::Panel1));
1381 let output_fold = if output_exp { "mobile-expanded" } else { "mobile-collapsed" };
1382 let panel2_class = format!("studio-panel {output_fold}{}{}", on_phone(MobileTab::Panel2), on_right(MobileTab::Panel2));
1383 let panel3_class = format!("studio-panel {output_fold}{}{}", on_phone(MobileTab::Panel3), on_right(MobileTab::Panel3));
1384
1385 let mobile_pct = *mobile_split.read();
1387 let panel1_flex = mobile_pct / 50.0;
1388 let panel2_flex = (100.0 - mobile_pct) / 50.0;
1389 let both_expanded = editor_exp && output_exp;
1390
1391 let panel1_style = if both_expanded {
1392 format!("--panel-flex: {panel1_flex};")
1393 } else {
1394 String::new()
1395 };
1396 let panel2_style = if both_expanded {
1397 format!("--panel-flex: {panel2_flex};")
1398 } else {
1399 String::new()
1400 };
1401
1402 let current_code_output_mode = *code_output_mode.read();
1404 let current_math_output_mode = *math_output_mode.read();
1405 let current_logic_output_mode = *logic_output_mode.read();
1406 let current_hw_output_mode = *hw_output_mode.read();
1407
1408 let (tab1_icon, tab1_label, tab2_icon, tab2_label, tab3_icon, tab3_label) = match current_mode {
1410 StudioMode::Logic => ("\u{270F}", "Input", "\u{2200}", "Logic", "", ""),
1411 StudioMode::Code => (
1412 "\u{03BB}",
1413 "Editor",
1414 "\u{276F}",
1415 "Output",
1416 "\u{1F4CB}",
1417 "Console",
1418 ),
1419 StudioMode::Math => (
1420 "\u{2200}",
1421 "Editor",
1422 "\u{276F}",
1423 "Output",
1424 "\u{1F4CB}",
1425 "Context",
1426 ),
1427 StudioMode::Hardware => ("\u{270F}", "Spec", "\u{22A8}", "SVA", "\u{25A6}", "Graph"),
1428 StudioMode::Web => ("\u{03BB}", "Source", "\u{25C9}", "Preview", "", ""),
1429 };
1430
1431 let (status_language, status_source) = match current_mode {
1434 StudioMode::Logic => (Language::Logic, String::new()),
1435 StudioMode::Code | StudioMode::Web => (Language::Logos, String::new()),
1436 StudioMode::Math => (Language::Vernacular, String::new()),
1437 StudioMode::Hardware => (Language::Hardware, hw_input.read().clone()),
1438 };
1439
1440 let walkthrough = Walkthrough { wrote: !input.read().trim().is_empty(), executed: result.read().logic.is_some() };
1442 let dock_text = match current_mode {
1443 StudioMode::Logic => dock_prompt(walkthrough, proof_hint.read().as_deref()),
1444 StudioMode::Code if debugging() => "Stepping through the program, one instruction at a time.".to_string(),
1445 StudioMode::Code => "Debug runs the program one instruction at a time, every variable in view.".to_string(),
1446 _ => String::new(),
1447 };
1448 let heal_source: Signal<String> = if current_mode == StudioMode::Logic { input } else { code_input };
1451 let heal_mode = if current_mode == StudioMode::Logic { heal::Mode::Logic } else { heal::Mode::Code };
1452 let handle_heal = use_callback(move |_: ()| {
1453 let text = heal_source.read().clone();
1454 let begun = heal::Flight::begin(&text);
1455 let flying = begun.busy();
1456 heal.set(begun);
1457 if !flying {
1458 return;
1459 }
1460 let baseline = current_file.peek().as_deref().and_then(|path| heal_baseline.peek().get(path).cloned()).unwrap_or_default();
1462 let regions = heal::edited_regions(&baseline, &text);
1463 if regions.is_empty() {
1464 heal.set(heal::Flight::Failed(heal::NOTHING_NEW.to_string()));
1465 return;
1466 }
1467 heal.set(heal::Flight::Flying { to: regions[0].start });
1468 spawn(async move {
1469 let mut mended = Vec::with_capacity(regions.len());
1470 for region in ®ions {
1471 let fragment = heal::region_text(&text, region);
1472 match heal::send(heal_mode, &fragment, true).await {
1473 Ok(lines) => mended.push(heal::reindent(&fragment, &lines)),
1474 Err(reason) => {
1475 heal.set(heal::Flight::Failed(reason));
1476 return;
1477 }
1478 }
1479 }
1480 let after = heal::splice(&text, ®ions, &mended);
1481 heal.set(heal::Flight::answered(text, Ok(after)));
1482 });
1483 });
1484 let heal_button = move || {
1485 let busy = heal.read().busy();
1486 rsx! {
1487 button {
1488 class: "execute-btn heal-btn",
1489 disabled: busy,
1490 title: "Mend the English in the editor, then keep or undo the mend",
1491 onclick: move |_| handle_heal.call(()),
1492 img { class: "heal-btn-icon", src: heal::FAIRY, alt: "" }
1493 span { class: "desktop-label", "Heal" }
1494 span { class: "mobile-label", "Heal" }
1495 }
1496 }
1497 };
1498
1499 let dock_text = heal::narration(&heal.read()).unwrap_or(dock_text);
1500
1501 let panels = rsx! {
1502 section {
1504 class: "{panel1_class} col-left",
1505 style: "{panel1_style}",
1506
1507 if matches!(current_mode, StudioMode::Code | StudioMode::Logic | StudioMode::Web) {
1509 HealOverlay { flight: heal, editor: heal_source, file: current_file, baseline: heal_baseline }
1510 }
1511
1512 match current_mode {
1513 StudioMode::Logic => rsx! {
1514 div {
1515 class: "panel-header",
1516 onclick: move |_| {
1517 if !*panes_fixed.peek() {
1518 let v = *editor_expanded.read();
1519 editor_expanded.set(!v);
1520 }
1521 },
1522 span { "English Input" }
1523 }
1524 div { class: "panel-content",
1525 CodeEditor {
1526 value: input.read().clone(),
1527 on_change: handle_logic_input,
1528 language: Language::Logic,
1529 language_server: true,
1530 status: editor_status,
1531 placeholder: "Type an English sentence...".to_string(),
1532 }
1533 }
1534 },
1535 StudioMode::Code | StudioMode::Web => rsx! {
1537 div {
1538 class: "panel-header",
1539 onclick: move |_| {
1540 if !*panes_fixed.peek() {
1541 let v = *editor_expanded.read();
1542 editor_expanded.set(!v);
1543 }
1544 },
1545 span { "Code Editor" }
1546 }
1547 div { class: "panel-content",
1548 CodeEditor {
1549 value: code_input.read().clone(),
1550 on_change: move |v| code_input.set(v),
1551 language: Language::Logos,
1552 language_server: true,
1553 status: editor_status,
1554 placeholder: "-- Imperative LOGOS code\n\n## Main\n\nLet x be 1.\nLet y be 2.\nShow x + y.".to_string(),
1555 }
1556 }
1557 },
1558 StudioMode::Math => rsx! {
1559 div {
1560 class: "panel-header",
1561 onclick: move |_| {
1562 if !*panes_fixed.peek() {
1563 let v = *editor_expanded.read();
1564 editor_expanded.set(!v);
1565 }
1566 },
1567 span { "Theorem Editor" }
1568 }
1569 div { class: "panel-content",
1570 CodeEditor {
1571 value: math_input.read().clone(),
1572 on_change: move |v| math_input.set(v),
1573 language: Language::Vernacular,
1574 status: editor_status,
1575 placeholder: "-- Define natural numbers\nInductive Nat := Zero : Nat | Succ : Nat -> Nat.\n\nDefinition one : Nat := Succ Zero.\n\nCheck one.".to_string(),
1576 }
1577 }
1578 },
1579 StudioMode::Hardware => rsx! {
1580 div {
1581 class: "panel-header",
1582 onclick: move |_| {
1583 if !*panes_fixed.peek() {
1584 let v = *editor_expanded.read();
1585 editor_expanded.set(!v);
1586 }
1587 },
1588 span { "Hardware Spec" }
1589 }
1590 div { class: "panel-content",
1591 CodeEditor {
1592 value: hw_input.read().clone(),
1593 on_change: move |v| hw_input.set(v),
1594 language: Language::Hardware,
1595 language_server: true,
1596 status: editor_status,
1597 placeholder: "Describe hardware in English (\"Always, if request is high, then acknowledge is high.\"), design a signal plan (\"NS-left conflicts with the EW crossing.\"), or paste Verilog (module \u{2026} endmodule) to model-check.".to_string(),
1598 }
1599 }
1600 },
1601 }
1602 }
1603
1604 if both_expanded {
1606 div {
1607 class: "mobile-panel-resizer",
1608 onpointerdown: move |evt| start_split_drag(evt),
1609 }
1610 }
1611
1612 div {
1613 class: "panel-resizer",
1614 "data-resizer": Resizer::LeftCenter.attr(),
1615 onpointerdown: move |evt| start_column_drag(evt, Resizer::LeftCenter),
1616 }
1617
1618 section {
1620 class: "{panel2_class} col-center",
1621 style: "{panel2_style}",
1622
1623 match current_mode {
1624 StudioMode::Logic => rsx! {
1625 div {
1626 class: "panel-header",
1627 onclick: move |_| {
1628 if !*panes_fixed.peek() {
1629 let v = *output_expanded.read();
1630 output_expanded.set(!v);
1631 }
1632 },
1633 span { "Logic Output" }
1634 div {
1635 class: "output-mode-toggle",
1636 onclick: move |evt| evt.stop_propagation(),
1637 button {
1638 class: if current_logic_output_mode == LogicView::Logic { "output-mode-btn active" } else { "output-mode-btn" },
1639 onclick: move |_| logic_output_mode.set(LogicView::Logic),
1640 "Logic"
1641 }
1642 button {
1643 class: if current_logic_output_mode == LogicView::Rust { "output-mode-btn active" } else { "output-mode-btn" },
1644 onclick: move |_| logic_output_mode.set(LogicView::Rust),
1645 "Rust"
1646 }
1647 }
1648 if current_logic_output_mode == LogicView::Logic {
1649 div {
1650 class: "format-toggle",
1651 onclick: move |evt| evt.stop_propagation(),
1652 button {
1653 class: if current_format == OutputFormat::SimpleFOL { "format-btn active" } else { "format-btn" },
1654 onclick: move |_| format.set(OutputFormat::SimpleFOL),
1655 "Simple"
1656 }
1657 button {
1658 class: if current_format == OutputFormat::Unicode { "format-btn active" } else { "format-btn" },
1659 onclick: move |_| format.set(OutputFormat::Unicode),
1660 "Full"
1661 }
1662 button {
1663 class: if current_format == OutputFormat::LaTeX { "format-btn active" } else { "format-btn" },
1664 onclick: move |_| format.set(OutputFormat::LaTeX),
1665 "LaTeX"
1666 }
1667 button {
1668 class: if current_format == OutputFormat::Kripke { "format-btn active" } else { "format-btn" },
1669 onclick: move |_| format.set(OutputFormat::Kripke),
1670 "Deep"
1671 }
1672 }
1673 }
1674 }
1675 div { class: "panel-content",
1676 if current_logic_output_mode == LogicView::Logic {
1677 {
1678 rsx! {
1679 LogicOutput {
1680 logic: current_result.logic.clone(),
1681 simple_logic: current_result.simple_logic.clone(),
1682 kripke_logic: current_result.kripke_logic.clone(),
1683 readings: current_result.readings.clone(),
1684 simple_readings: current_result.simple_readings.clone(),
1685 kripke_readings: current_result.kripke_readings.clone(),
1686 error: current_result.error.clone(),
1687 format: current_format,
1688 }
1689 if let Some(symbols) = dictionary() {
1691 Collapsible {
1692 title: "Symbol Dictionary",
1693 open: dictionary_open,
1694 SymbolGroups { symbols }
1695 }
1696 }
1697 if let Some(ast) = current_result.ast.clone() {
1698 Collapsible {
1699 title: "Syntax Tree",
1700 open: tree_open,
1701 AstTree { ast }
1702 }
1703 }
1704 }
1705 }
1706 } else {
1707 {
1708 let rust_code = generated_logic_rust.read().clone();
1709 rsx! {
1710 if rust_code.is_empty() {
1711 div { class: "interpreter-empty",
1712 "Compile to see generated Rust"
1713 }
1714 } else {
1715 CodeView {
1716 code: rust_code,
1717 language: Language::Rust,
1718 }
1719 }
1720 }
1721 }
1722 }
1723 }
1724 },
1725 StudioMode::Code => rsx! {
1726 div {
1727 class: "panel-header",
1728 onclick: move |_| {
1729 if !*panes_fixed.peek() {
1730 let v = *output_expanded.read();
1731 output_expanded.set(!v);
1732 }
1733 },
1734 span { "Output" }
1735 div {
1736 class: "output-mode-toggle",
1737 onclick: move |evt| evt.stop_propagation(),
1738 button {
1739 class: if current_code_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
1740 onclick: move |_| code_output_mode.set(CodeOutputMode::Interpret),
1741 "Output"
1742 }
1743 button {
1744 class: if current_code_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
1745 onclick: move |_| code_output_mode.set(CodeOutputMode::Rust),
1746 "Rust"
1747 }
1748 }
1749 }
1750 div { class: "panel-content",
1751 if current_code_output_mode == CodeOutputMode::Interpret {
1752 {
1753 let stepping = debugging();
1756 let result = if stepping { debug_output.read() } else { interpreter_result.read() };
1757 let runtime_notice = current_file.read().as_deref().and_then(compiled_runtime_notice);
1758 rsx! {
1759 div { class: "interpreter-output",
1760 if let Some(notice) = &runtime_notice {
1761 div { class: "panel-verdict", "{notice}" }
1762 }
1763 if result.lines.is_empty() && result.error.is_none() {
1764 if stepping {
1765 div { class: "interpreter-empty",
1766 "Nothing printed yet. What the program shows appears here."
1767 }
1768 } else if runtime_notice.is_none() {
1769 div { class: "interpreter-empty",
1770 "Run your code to see output"
1771 }
1772 }
1773 } else {
1774 for line in result.lines.iter() {
1775 div { class: "interpreter-line", "{line}" }
1776 }
1777 if let Some(ref err) = result.error {
1778 div { class: "interpreter-error", "{err}" }
1779 }
1780 }
1781 }
1782 }
1783 }
1784 } else {
1785 {
1786 let rust_code = generated_rust.read().clone();
1787 rsx! {
1788 if rust_code.is_empty() {
1789 div { class: "interpreter-empty",
1790 "Compile your code to see generated Rust"
1791 }
1792 } else {
1793 CodeView {
1794 code: rust_code,
1795 language: Language::Rust,
1796 }
1797 }
1798 }
1799 }
1800 }
1801 }
1802 },
1803 StudioMode::Math => rsx! {
1804 div {
1805 class: "panel-header",
1806 onclick: move |_| {
1807 if !*panes_fixed.peek() {
1808 let v = *output_expanded.read();
1809 output_expanded.set(!v);
1810 }
1811 },
1812 span { "Output" }
1813 div {
1814 class: "panel-header-actions",
1815 onclick: move |evt| evt.stop_propagation(),
1816 if current_math_output_mode == CodeOutputMode::Interpret && !math_output.read().is_empty() {
1817 div { class: "output-mode-toggle",
1818 button {
1819 class: "output-mode-btn",
1820 onclick: move |_| {
1821 math_output.write().clear();
1822 math_context.set(MathContext::prelude());
1824 },
1825 "Clear"
1826 }
1827 }
1828 }
1829 div { class: "output-mode-toggle",
1830 button {
1831 class: if current_math_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
1832 onclick: move |_| math_output_mode.set(CodeOutputMode::Interpret),
1833 "Output"
1834 }
1835 button {
1836 class: if current_math_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
1837 onclick: move |_| math_output_mode.set(CodeOutputMode::Rust),
1838 "Rust"
1839 }
1840 }
1841 }
1842 }
1843 div { class: "panel-content",
1844 if current_math_output_mode == CodeOutputMode::Interpret {
1845 ReplOutput {
1846 lines: math_output.read().clone(),
1847 }
1848 } else {
1849 {
1850 let rust_code = generated_math_rust.read().clone();
1851 rsx! {
1852 if rust_code.is_empty() {
1853 div { class: "interpreter-empty",
1854 "Compile to see generated Rust"
1855 }
1856 } else {
1857 CodeView {
1858 code: rust_code,
1859 language: Language::Rust,
1860 }
1861 }
1862 }
1863 }
1864 }
1865 }
1866 },
1867 StudioMode::Hardware => {
1868 let report = hw_report.read().clone();
1869 let title = hw_kind.map(HardwareKind::result_title).unwrap_or("SVA");
1870 let rust_view = hw_kind.map_or(true, HardwareKind::compiles);
1871 let planted = current_file.read().as_deref().and_then(planted_bug_notice);
1872 let compiled = hw_compiled.read().clone();
1873 rsx! {
1874 div {
1875 class: "panel-header",
1876 onclick: move |_| {
1877 if !*panes_fixed.peek() {
1878 let v = *output_expanded.read();
1879 output_expanded.set(!v);
1880 }
1881 },
1882 span { "{title}" }
1883 if rust_view {
1884 div {
1885 class: "output-mode-toggle",
1886 onclick: move |evt| evt.stop_propagation(),
1887 button {
1888 class: if current_hw_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
1889 onclick: move |_| hw_output_mode.set(CodeOutputMode::Interpret),
1890 "SVA"
1891 }
1892 button {
1893 class: if current_hw_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
1894 onclick: move |_| hw_output_mode.set(CodeOutputMode::Rust),
1895 "Rust"
1896 }
1897 }
1898 }
1899 }
1900 div { class: "panel-content",
1901 if current_hw_output_mode == CodeOutputMode::Rust && rust_view {
1902 {
1903 let rust_code = generated_hw_rust.read().clone();
1904 rsx! {
1905 if rust_code.is_empty() {
1906 div { class: "interpreter-empty",
1907 "Compile to see the Rust runtime monitor"
1908 }
1909 } else {
1910 CodeView {
1911 code: rust_code,
1912 language: Language::Rust,
1913 }
1914 }
1915 }
1916 }
1917 } else {
1918 div { class: "interpreter-output",
1919 match compiled {
1920 Some(Compiled::Nothing(note)) => rsx! { div { class: "panel-verdict", "{note}" } },
1921 Some(Compiled::Failed(e)) => rsx! { div { class: "interpreter-error", "{e}" } },
1922 Some(Compiled::Rust(_)) | None => rsx! {},
1923 }
1924 if let Some(notice) = &planted {
1925 div { class: "panel-verdict", "{notice}" }
1926 }
1927 match report {
1928 None => rsx! {
1929 div { class: "interpreter-empty",
1930 "Execute to analyze: synthesize an assertion, design a signal plan, or model-check Verilog"
1931 }
1932 },
1933 Some((_, report)) => rsx! {
1934 if let Some(e) = &report.error {
1935 div { class: "interpreter-error", "{e}" }
1936 }
1937 for section in report.sections.iter() {
1938 div { class: "panel-section-label", "{section.label}" }
1939 pre { class: "panel-code", "{section.body}" }
1940 }
1941 if let Some((text, ok)) = &report.verdict {
1942 div {
1943 class: match ok {
1944 Some(true) => "panel-verdict proved",
1945 Some(false) => "panel-verdict refuted",
1946 None => "panel-verdict",
1947 },
1948 "{text}"
1949 }
1950 }
1951 },
1952 }
1953 }
1954 }
1955 }
1956 }
1957 },
1958 StudioMode::Web => {
1961 let doc = web_preview.read().clone();
1962 rsx! {
1963 div { class: "panel-header", span { "Preview" } }
1964 div { class: "panel-content", style: "padding: 0; overflow: hidden;",
1965 iframe {
1966 title: PREVIEW_TITLE,
1967 srcdoc: "{doc}",
1968 "sandbox": "allow-scripts",
1969 style: "width: 100%; height: 100%; border: 0; background: #faf9f5;",
1970 }
1971 }
1972 }
1973 },
1974 }
1975 }
1976
1977 if show_panel3 {
1979 div {
1980 class: "panel-resizer",
1981 "data-resizer": Resizer::CenterRight.attr(),
1982 onpointerdown: move |evt| start_column_drag(evt, Resizer::CenterRight),
1983 }
1984
1985 aside {
1986 class: "{panel3_class} col-right",
1987 style: "{panel2_style}",
1988
1989 match current_mode {
1990 StudioMode::Code => rsx! {
1991 div { class: "panel-header",
1992 span { "Console" }
1993 }
1994 div { class: "panel-content",
1995 div { class: "interpreter-output",
1996 {
1997 let result = interpreter_result.read();
1998 rsx! {
1999 if let Some(ref err) = result.error {
2000 div { class: "interpreter-error", "{err}" }
2001 }
2002 }
2003 }
2004 }
2005 }
2006 },
2007 StudioMode::Math => rsx! {
2008 div { class: "panel-header",
2009 span { "Context" }
2010 }
2011 div { class: "panel-content",
2012 ContextView {
2013 definitions: math_context.read().definitions.clone(),
2014 inductives: math_context.read().inductives.clone(),
2015 }
2016 }
2017 },
2018 StudioMode::Logic | StudioMode::Web => rsx! {},
2020 StudioMode::Hardware => {
2021 let report = hw_report.read().clone().map(|(_, report)| report).unwrap_or_default();
2022 let violated = report.verdict.as_ref().is_some_and(|(_, ok)| *ok == Some(false));
2023 if let (Some(HardwareKind::Pigeonhole), Some(pspec)) = (hw_kind, crate::ui::pages::pigeonhole_viz::parse_pigeonhole_spec(&hw_input.read())) {
2024 let (svg, caption) = crate::ui::pages::pigeonhole_viz::render(&pspec);
2028 rsx! {
2029 div { class: "panel-header", span { "Pigeonhole" } }
2030 div { class: "panel-content",
2031 div { class: "panel-body wide",
2032 div { dangerous_inner_html: "{svg}" }
2033 div { class: "panel-caption", "{caption}" }
2034 }
2035 }
2036 }
2037 } else if let (Some(HardwareKind::RegisterAlloc), Some(spec)) = (hw_kind, crate::ui::pages::register_alloc_viz::parse_register_spec(&hw_input.read())) {
2038 let (svg, _) = crate::ui::pages::register_alloc_viz::render(&spec);
2041 rsx! {
2042 div { class: "panel-header", span { "Register Allocation" } }
2043 div { class: "panel-content",
2044 div { class: "panel-body wide",
2045 div { dangerous_inner_html: "{svg}" }
2046 div { class: "panel-caption",
2047 "The sweep line is the program counter. Each register lane lights up while it holds a value and goes dark when it's freed. The pressure histogram counts how many values are live at each instruction, and the bars above the dashed budget line turn red: the exact points where spilling is forced and a value drops to the MEM lane. The interference graph is what allocation really colours: an edge joins two values whose lifetimes overlap, nodes are coloured by their register, and a red clique is a set that mutually conflicts and provably cannot share."
2048 }
2049 }
2050 }
2051 }
2052 } else if !report.trace.is_empty() {
2053 let wf = logicaffeine_compile::codegen_sva::hw_pipeline::counterexample_waveform(&report.trace);
2055 let traffic = traffic_svg(&wf);
2056 let osc = waveform_svg(&wf);
2057 let header = match (traffic.is_some(), violated) {
2058 (true, true) => "Intersection: Conflict Found",
2059 (true, false) => "Intersection: Proven Safe (witness run)",
2060 (false, true) => "Counterexample Waveform",
2061 (false, false) => "Witness Waveform",
2062 };
2063 rsx! {
2064 div { class: "panel-header", span { "{header}" } }
2065 div { class: "panel-content",
2066 div { class: "panel-body wide",
2067 if let Some(t) = traffic.as_ref() {
2068 div { dangerous_inner_html: "{t}" }
2069 }
2070 div { dangerous_inner_html: "{osc}" }
2071 }
2072 }
2073 }
2074 } else {
2075 let svg = kg_svg(&report.kg);
2076 rsx! {
2077 div { class: "panel-header", span { if svg.is_empty() { "Signals" } else { "Knowledge Graph" } } }
2078 div { class: "panel-content",
2079 if !svg.is_empty() {
2080 div { class: "panel-body",
2081 div { dangerous_inner_html: "{svg}" }
2082 div { style: "display:flex; gap:12px; flex-wrap:wrap; justify-content:center; margin-top:4px; font-size:10px;",
2083 span { style: "color:#60a5fa;", "● input" }
2084 span { style: "color:#4ade80;", "● output" }
2085 span { style: "color:#a78bfa;", "● internal" }
2086 span { style: "color:#fbbf24;", "● clock" }
2087 }
2088 }
2089 } else if !report.signals.is_empty() {
2090 div { class: "panel-body",
2091 for s in report.signals.iter() {
2092 div { style: "font-family: ui-monospace, monospace; font-size: 13px; color: #e5e7eb; padding: 2px 0;", "{s}" }
2093 }
2094 }
2095 } else {
2096 div { class: "interpreter-empty",
2097 "Execute to extract the hardware knowledge graph"
2098 }
2099 }
2100 }
2101 }
2102 }
2103 },
2104 }
2105 }
2106 }
2107 };
2108
2109 let withheld: Vec<(&'static str, String)> = {
2111 let actions = actions();
2112 let run = if current_mode == StudioMode::Code { "Run" } else { "Execute" };
2113 let mut shown = vec![(run, &actions.run), ("Compile", &actions.compile)];
2114 if current_mode == StudioMode::Code && host == Host::Page {
2115 shown.push(("Debug", &actions.debug));
2116 }
2117 if current_mode == StudioMode::Web {
2118 shown.clear();
2119 }
2120 shown.into_iter().filter_map(|(name, offer)| offer.reason().map(|why| (name, why.to_string()))).collect()
2121 };
2122 let inset = *keyboard_inset.read();
2123
2124 rsx! {
2125 div { class: "studio-workbench", style: "--keyboard-inset: {inset}px;",
2126 div { class: "studio-toolbar",
2127 div { class: "studio-toolbar-left",
2128 {
2129 let open = if *drawer.read() { *drawer_open.read() } else { *sidebar_open.read() };
2130 let label = if open { "Hide files" } else { "Show files" };
2131 rsx! {
2132 button {
2133 class: "sidebar-toggle-btn",
2134 onclick: move |_| if *drawer.peek() { drawer_open.set(!open) } else { sidebar_open.set(!open) },
2135 title: "{label}",
2136 aria_label: "{label}",
2137 aria_expanded: "{open}",
2138 span {
2139 class: "toggle-icon",
2140 dangerous_inner_html: if open { COLLAPSE_FILES_SVG } else { FILES_SVG },
2141 }
2142 }
2143 if !open {
2145 span { class: "files-label", "Files" }
2146 }
2147 }
2148 }
2149 }
2150 div { class: "studio-toolbar-right",
2151 if let Some(stopped) = fault() {
2152 span {
2153 class: "studio-fault",
2154 role: "alert",
2155 title: "{stopped.detail()}",
2156 onclick: move |_| fault_open.set(!fault_open()),
2157 "\u{26A0} {stopped.headline()}"
2158 }
2159 }
2160 if current_mode == StudioMode::Code {
2161 button {
2162 class: "execute-btn",
2163 disabled: !actions().run.is_ready(),
2164 title: actions().run.reason().map(str::to_string),
2165 onclick: handle_code_run,
2166 span { class: "desktop-label", "\u{25B6} Run" }
2167 span { class: "mobile-label", "\u{25B6} Run" }
2168 }
2169 {heal_button()}
2170 button {
2171 class: "execute-btn compile-btn",
2172 style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
2173 disabled: !actions().compile.is_ready(),
2174 title: actions().compile.reason().map(str::to_string),
2175 onclick: handle_code_compile,
2176 span { class: "desktop-label", "\u{1F980} Compile" }
2177 span { class: "mobile-label", "\u{1F980} Compile to Rust" }
2178 }
2179 if host == Host::Page {
2180 button {
2181 class: "execute-btn",
2182 style: "background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%); display:inline-flex; align-items:center; gap:6px;",
2183 disabled: !actions().debug.is_ready(),
2184 title: actions().debug.reason().map(str::to_string),
2185 onclick: move |_| {
2186 code_output_mode.set(CodeOutputMode::Interpret);
2187 debugging.set(true);
2188 },
2189 span { style: "display:inline-flex;", dangerous_inner_html: IC_BUG }
2190 span { class: "desktop-label", "Debug" }
2191 span { class: "mobile-label", "Debug" }
2192 }
2193 }
2194 }
2195 if current_mode == StudioMode::Math {
2196 button {
2197 class: "execute-btn",
2198 disabled: !actions().run.is_ready(),
2199 title: actions().run.reason().map(str::to_string),
2200 onclick: handle_math_execute,
2201 span { class: "desktop-label", "\u{25B6} Execute" }
2202 span { class: "mobile-label", "\u{25B6} Execute" }
2203 }
2204 button {
2205 class: "execute-btn compile-btn",
2206 style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
2207 disabled: !actions().compile.is_ready(),
2208 title: actions().compile.reason().map(str::to_string),
2209 onclick: handle_math_compile,
2210 span { class: "desktop-label", "\u{1F980} Compile" }
2211 span { class: "mobile-label", "\u{1F980} Compile to Rust" }
2212 }
2213 }
2214 if current_mode == StudioMode::Logic {
2215 button {
2216 class: "execute-btn",
2217 disabled: !actions().run.is_ready(),
2218 title: actions().run.reason().map(str::to_string),
2219 onclick: handle_logic_execute,
2220 span { class: "desktop-label", "\u{25B6} Execute" }
2221 span { class: "mobile-label", "\u{25B6} Execute" }
2222 }
2223 {heal_button()}
2224 button {
2225 class: "execute-btn compile-btn",
2226 style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
2227 disabled: !actions().compile.is_ready(),
2228 title: actions().compile.reason().map(str::to_string),
2229 onclick: handle_logic_compile,
2230 span { class: "desktop-label", "\u{1F980} Compile" }
2231 span { class: "mobile-label", "\u{1F980} Compile to Rust" }
2232 }
2233 }
2234 if current_mode == StudioMode::Hardware {
2235 button {
2236 class: "execute-btn",
2237 disabled: !actions().run.is_ready(),
2238 title: actions().run.reason().map(str::to_string),
2239 onclick: handle_hardware_execute,
2240 span { class: "desktop-label", "\u{25B6} Execute" }
2241 span { class: "mobile-label", "\u{25B6} Execute" }
2242 }
2243 button {
2244 class: "execute-btn compile-btn",
2245 style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
2246 disabled: !actions().compile.is_ready(),
2247 title: actions().compile.reason().map(str::to_string),
2248 onclick: handle_hardware_compile,
2249 span { class: "desktop-label", "\u{1F980} Compile" }
2250 span { class: "mobile-label", "\u{1F980} Compile to Rust" }
2251 }
2252 }
2253 if current_mode == StudioMode::Web {
2255 {heal_button()}
2256 }
2257 if host == Host::Frame {
2259 if let Some(open) = current_file.read().clone() {
2260 Link { to: studio_file_url(&open), class: "studio-open", "Open in Studio" }
2261 }
2262 }
2263 }
2264 }
2265
2266 if let Some(stopped) = fault().filter(|_| fault_open()) {
2269 div { class: "studio-fault-detail", "{stopped.detail()}" }
2270 }
2271 if !withheld.is_empty() {
2272 div { class: "studio-reasons",
2273 for (name, why) in withheld {
2274 span { key: "{name}", "{name}: {why}" }
2275 }
2276 }
2277 }
2278
2279 nav { class: if show_panel3 { "mobile-tabs has-third" } else { "mobile-tabs" }, role: "tablist", "aria-label": "Panes",
2281 for (tab, icon, label) in [(MobileTab::Panel1, tab1_icon, tab1_label), (MobileTab::Panel2, tab2_icon, tab2_label), (MobileTab::Panel3, tab3_icon, tab3_label)] {
2282 if tab != MobileTab::Panel3 || show_panel3 {
2283 button {
2284 class: if current_tab == tab { "mobile-tab active" } else { "mobile-tab" },
2285 role: "tab",
2286 "aria-selected": if current_tab == tab { "true" } else { "false" },
2287 onclick: move |_| active_tab.set(tab),
2288 span { class: "mobile-tab-icon", "aria-hidden": "true", "{icon}" }
2289 span { class: "mobile-tab-label", "{label}" }
2290 }
2291 }
2292 }
2293 }
2294
2295 if current_mode == StudioMode::Logic {
2297 div { class: "studio-guide",
2298 SocraticGuide {
2299 mode: guide_mode.clone(),
2300 on_hint_request: None,
2301 }
2302 }
2303 }
2304
2305 div { class: "studio-content", style: columns_style,
2307 if *sidebar_open.read() || *drawer_open.read() {
2310 div {
2311 class: if *drawer_open.read() { "sidebar-overlay drawer-open" } else { "sidebar-overlay" },
2312 onclick: move |_| drawer_open.set(false),
2313 }
2314 }
2315
2316 if *sidebar_open.read() || *drawer_open.read() {
2317 div {
2318 class: if *drawer_open.read() { "studio-sidebar drawer-open" } else { "studio-sidebar" },
2319
2320 if let Some(err) = store.error.as_ref() {
2322 div {
2323 style: "background: #ff4444; color: white; padding: 6px 12px; font-size: 11px; border-radius: 0;",
2324 "VFS Error: {err}"
2325 }
2326 }
2327
2328 FileBrowser {
2329 show_private_mode: store.private_mode,
2330 new_file_waits: store.new_file_waits.clone(),
2331 tree: file_tree.read().clone(),
2332 selected_path: current_file.read().clone(),
2333 on_select: EventHandler::new(move |path: String| {
2334 state.choose(path.clone());
2337 if let Some(chosen) = on_choose {
2338 chosen.call(path);
2339 }
2340 }),
2341 on_toggle_dir: EventHandler::new(move |path: String| {
2342 if let Some(node) = file_tree.write().find_mut(&path) {
2343 node.toggle_expanded();
2344 }
2345 }),
2346 on_new_file,
2347 }
2348 }
2349 if *sidebar_open.read() {
2350 div {
2351 class: "panel-resizer",
2352 "data-resizer": Resizer::SidebarLeft.attr(),
2353 onpointerdown: move |evt| start_column_drag(evt, Resizer::SidebarLeft),
2354 }
2355 }
2356 }
2357
2358 if host == Host::Page {
2360 main { class: "studio-main", {panels} }
2361 } else {
2362 div { class: "studio-main", {panels} }
2363 }
2364 }
2365
2366 if current_mode == StudioMode::Logic && *proof_open.read() {
2369 ProofPanel {
2370 proof_text: proof_text.read().clone(),
2371 tree: proof_tree.read().clone(),
2372 status: proof_status.read().clone(),
2373 steps: walkthrough,
2374 height: proof_height,
2375 on_prove: move |_| handle_prove.call(()),
2376 on_close: move |_| proof_open.set(false),
2377 }
2378 }
2379 if debugging() && current_mode == StudioMode::Code {
2380 DebugDrawer {
2381 source: code_input.read().clone(),
2382 height: drawer_height,
2383 output: debug_output,
2384 on_close: move |_| debugging.set(false),
2385 }
2386 }
2387 if current_mode == StudioMode::Logic {
2390 div { class: "studio-dock",
2391 span { class: "studio-dock-icon", "aria-hidden": "true", "\u{1F4A1}" }
2392 span { class: "studio-dock-text", role: "status", "{dock_text}" }
2393 button {
2394 class: "studio-dock-btn primary",
2395 title: "Ask the kernel whether the first sentence follows",
2396 onclick: move |_| handle_prove.call(()),
2397 "\u{25B6} Prove"
2398 }
2399 button {
2400 class: "studio-dock-btn",
2401 aria_expanded: if *proof_open.read() { "true" } else { "false" },
2402 onclick: move |_| {
2403 let open = *proof_open.read();
2404 proof_open.set(!open);
2405 },
2406 if *proof_open.read() { "Hide proof" } else { "Show proof" }
2407 }
2408 }
2409 }
2410 if current_mode == StudioMode::Code {
2411 div { class: "studio-dock",
2412 span { class: "studio-dock-icon", "aria-hidden": "true", dangerous_inner_html: IC_BUG }
2413 span { class: "studio-dock-text", role: "status", "{dock_text}" }
2414 if debugging() {
2415 button { class: "studio-dock-btn", onclick: move |_| debugging.set(false), "Stop debugging" }
2416 } else {
2417 button {
2418 class: "studio-dock-btn primary",
2419 disabled: !actions().debug.is_ready(),
2420 title: actions().debug.reason().map(str::to_string),
2421 onclick: move |_| {
2422 code_output_mode.set(CodeOutputMode::Interpret);
2423 debugging.set(true);
2424 },
2425 "Debug"
2426 }
2427 }
2428 }
2429 }
2430
2431 StatusBar {
2432 status: editor_status,
2433 language: status_language.name(&status_source).to_string(),
2434 indent: Indentation::of(status_language, &status_source).step.len(),
2435 problems: current_mode != StudioMode::Math,
2436 }
2437 }
2438 }
2439}
2440
2441#[cfg(test)]
2442#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/pages/studio_workbench/tests.rs"]
2443mod tests;
2444
2445#[component]
2450fn HealOverlay(flight: Signal<heal::Flight>, editor: Signal<String>, file: Signal<Option<String>>, baseline: Signal<std::collections::HashMap<String, String>>) -> Element {
2451 let state = flight.read().clone();
2452 if state == heal::Flight::Idle {
2453 return rsx! {};
2454 }
2455 let line = state.line();
2456 let mended = match &state {
2457 heal::Flight::Review { lines, .. } => format!("{} place{} mended. Nothing changes until Keep.", lines.len(), if lines.len() == 1 { "" } else { "s" }),
2458 _ => String::new(),
2459 };
2460 let phase = match &state {
2461 heal::Flight::Flying { .. } => "flying",
2462 heal::Flight::Mending => "mending",
2463 heal::Flight::Review { .. } => "review",
2464 heal::Flight::Failed(_) => "failed",
2465 heal::Flight::Idle => "idle",
2466 };
2467 #[cfg(target_arch = "wasm32")]
2469 {
2470 let closable = matches!(state, heal::Flight::Review { .. } | heal::Flight::Failed(_));
2471 use_effect(move || {
2472 if !closable {
2473 return;
2474 }
2475 use wasm_bindgen::{closure::Closure, JsCast};
2476 let Some(window) = web_sys::window() else { return };
2477 let on_key = Closure::<dyn FnMut(web_sys::KeyboardEvent)>::new(move |event: web_sys::KeyboardEvent| {
2478 if event.key() == "Escape" {
2479 let next = flight.peek().clone().undo();
2480 flight.set(next);
2481 }
2482 });
2483 let _ = window.add_event_listener_with_callback("keydown", on_key.as_ref().unchecked_ref());
2484 on_key.forget();
2485 });
2486 }
2487 rsx! {
2488 div { class: "heal-overlay", style: "--heal-line: {line};",
2489 img { class: "heal-fairy {phase}", src: heal::FAIRY, alt: "The fairy", "aria-hidden": "true" }
2490 match &state {
2491 heal::Flight::Review { before, after, diff, .. } => rsx! {
2492 div { class: "heal-card", role: "dialog", "aria-label": "Review the mend",
2493 div { class: "heal-card-cols",
2494 section { class: "heal-card-col",
2495 h3 { "Before" }
2496 pre { class: "heal-before", {marked_lines(before, after, &diff.before, &diff.after, "replaced")} }
2497 }
2498 section { class: "heal-card-col",
2499 h3 { "After" }
2500 pre { class: "heal-after", {marked_lines(after, before, &diff.after, &diff.before, "mended")} }
2501 }
2502 }
2503 div { class: "heal-card-actions",
2504 span { class: "heal-card-note", "{mended}" }
2505 button {
2506 class: "studio-dock-btn",
2507 onclick: move |_| { let next = flight.peek().clone().undo(); flight.set(next); },
2508 "Undo"
2509 }
2510 button {
2511 class: "studio-dock-btn primary heal-keep",
2512 onclick: move |_| {
2513 let (next, kept) = flight.peek().clone().keep();
2514 if let Some(text) = kept {
2515 editor.set(text.clone());
2516 if let Some(path) = file.peek().clone() {
2517 baseline.write().insert(path, text);
2518 }
2519 }
2520 flight.set(next);
2521 },
2522 "Keep"
2523 }
2524 }
2525 }
2526 },
2527 heal::Flight::Failed(reason) => rsx! {
2528 div { class: "heal-card heal-card-failed", role: "alert",
2529 p { "{reason}" }
2530 button {
2531 class: "studio-dock-btn",
2532 onclick: move |_| { let next = flight.peek().clone().undo(); flight.set(next); },
2533 "Close"
2534 }
2535 }
2536 },
2537 _ => rsx! {},
2538 }
2539 }
2540 }
2541}
2542
2543fn marked_lines(text: &str, other_text: &str, own: &[std::ops::Range<usize>], other: &[std::ops::Range<usize>], class: &'static str) -> Element {
2547 let other_lines: Vec<&str> = other_text.lines().collect();
2548 let counterpart = |i: usize| -> Option<&str> {
2549 let (k, r) = own.iter().enumerate().find(|(_, r)| r.contains(&i))?;
2550 let o = other.get(k)?;
2551 (r.len() == 1 && o.len() == 1).then(|| other_lines.get(o.start).copied()).flatten()
2552 };
2553 rsx! {
2554 for (i, line) in text.lines().enumerate() {
2555 if own.iter().any(|r| r.contains(&i)) {
2556 span { class: "heal-line {class}",
2557 if let Some(pair) = counterpart(i) {
2558 for (piece, differs) in heal::word_marks(line, pair) {
2559 span { class: if differs { "heal-word" } else { "" }, "{piece}" }
2560 }
2561 } else {
2562 "{line}"
2563 }
2564 "\n"
2565 }
2566 } else {
2567 span { class: "heal-line", "{line}\n" }
2568 }
2569 }
2570 }
2571}