use egui::TextEdit; use egui_commonmark::{CommonMarkCache, CommonMarkViewer}; use serde::{self, Deserialize, Serialize}; use crate::{ FILESYSTEM, PROJECT_FOLDER, editors::{settings_editor::ProjectSettings, tags::Tag}, filesystem::{FileSystem, Id}, util, }; #[cfg(feature = "llm")] use crate::llm_integration::content_llm::{ContentAI, ReadyState}; pub struct MainEditor { pub content: ContentSection, pub show_editor: bool, pub editor_separate_window: bool, pub show_preview: bool, preview_cache: CommonMarkCache, #[cfg(feature = "llm")] dialog: ContentAI, #[cfg(feature = "llm")] pub show_ai: bool, } impl Clone for MainEditor { fn clone(&self) -> Self { Self { content: self.content.clone(), show_editor: self.show_editor, editor_separate_window: self.editor_separate_window, show_preview: self.show_preview, preview_cache: CommonMarkCache::default(), #[cfg(feature = "llm")] dialog: self.dialog.clone(), #[cfg(feature = "llm")] show_ai: self.show_ai, } } } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ContentSection { #[serde(default)] pub title: String, pub id: Id, #[serde(default)] pub description: String, #[serde(default)] pub tags: Vec, #[serde(default)] pub content: String, // parent id #[serde(default)] pub parent: Option, #[serde(skip)] pub saved: bool, } impl ContentSection { pub fn new() -> Self { Self { title: String::new(), id: Id::new(), description: String::new(), tags: Vec::new(), content: String::new(), parent: None, saved: false, } } pub fn save( &mut self, filesystem: &F, ) -> Result<(), Box> { let documents_dir = PROJECT_FOLDER.join("documents"); if filesystem.exists(&self.id) { filesystem.write(&self.id, self.clone())?; } else { let _new_id = filesystem.create(&documents_dir, self.clone())?; // Note: The filesystem creates its own ID, but we keep our existing ID for consistency } self.saved = true; Ok(()) } pub fn load( filesystem: &F, id: &Id, ) -> Result> { let mut section: Self = filesystem.read(id)?; section.saved = true; section.id = id.clone(); Ok(section) } pub fn create_child(&self) -> Self { let mut child = Self::new(); child.title = format!("{} (Child)", self.title); child.parent = Some(self.id.clone()); child } } impl MainEditor { pub fn new() -> Self { Self { content: ContentSection::new(), show_editor: false, // Start with editor hidden show_preview: false, editor_separate_window: false, preview_cache: CommonMarkCache::default(), #[cfg(feature = "llm")] show_ai: false, #[cfg(feature = "llm")] dialog: ContentAI::new(String::new()), } } pub fn open(content: ContentSection) -> Self { Self { content, show_editor: true, show_preview: false, editor_separate_window: false, preview_cache: CommonMarkCache::default(), #[cfg(feature = "llm")] show_ai: false, #[cfg(feature = "llm")] dialog: ContentAI::new(String::new()), } } pub fn render_ui( &mut self, project: &mut ProjectSettings, filesystem: &F, ui: &mut egui::Ui, ) { ui.vertical(|ui| { // check for Ctrl+S to save if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) { if let Err(e) = self.content.save(filesystem) { eprintln!("Failed to save: {e}"); } } // display save state util::saved_status( ui, self.content.saved, &self.content.id, &self.content.title, ); // Save/Cancel buttons ui.horizontal(|ui| { // save button if ui.button("Save").clicked() { if let Err(e) = self.content.save(filesystem) { eprintln!("Failed to save: {e}"); } } // create copy button if ui.button("Create Copy").clicked() { let mut copy = self.clone(); copy.content.id = Id::new(); copy.content.title = format!("{} (Copy)", self.content.title); FILESYSTEM.clone(&self.content.id, ©.content.id); // TODO: Fix save call to pass filesystem // copy.content.save().unwrap(); } // delete button if ui.button("Delete").clicked() { filesystem.delete(&self.content.id).unwrap(); *self = Self::new(); } // revert changes button if ui.button("Revert changes").clicked() { self.content = ContentSection::load(filesystem, &self.content.id).unwrap(); } // preview toggle ui.checkbox(&mut self.show_preview, "Preview"); // assistant toggle #[cfg(feature = "llm")] ui.checkbox(&mut self.show_ai, "AI Assistant"); // editor toggle ui.checkbox(&mut self.editor_separate_window, "Pop out editor"); }); }); ui.separator(); // Name and description grid egui::Grid::new("top_grid") .striped(true) .num_columns(2) .show(ui, |ui| { ui.strong("Name"); if ui .add( TextEdit::singleline(&mut self.content.title) .desired_width(f32::INFINITY) .frame(false), ) .changed() { self.content.saved = false; } ui.end_row(); ui.strong("Description"); if ui .add( TextEdit::singleline(&mut self.content.description) .desired_width(f32::INFINITY) .frame(false), ) .changed() { self.content.saved = false; } ui.end_row(); ui.strong("Tags"); Tag::selector_ui(&mut self.content.tags, ui, Some(&mut self.content.saved)); ui.end_row(); }); ui.separator(); #[cfg(feature = "llm")] if self.show_ai { let dialog = &mut self.dialog; dialog.content = self.content.content.clone(); dialog.ui(ui, project); if *dialog.ready.lock().unwrap() == ReadyState::Ready { self.content .content .push_str(&dialog.result.lock().unwrap()); self.content.saved = false; *dialog.ready.lock().unwrap() = ReadyState::Idle; } else if *dialog.ready.lock().unwrap() == ReadyState::Halted { *dialog.ready.lock().unwrap() = ReadyState::Idle; } } if self.show_preview { self.preview_ui(ui); } self.editor_ui(ui, project); } pub fn ui( &mut self, ctx: &egui::Context, project: &mut ProjectSettings, filesystem: &F, ) { // Show the editor window if enabled let mut show = self.show_editor; if show { if self.editor_separate_window { egui::Window::new("Editor") .resizable(true) .default_width(1000.0) .default_height(800.0) .open(&mut show) .show(ctx, |ui| { self.render_ui(project, filesystem, ui); }); } else { egui::CentralPanel::default().show(ctx, |ui| { self.render_ui(project, filesystem, ui); }); } } self.show_editor = show; } fn preview_ui(&mut self, ui: &mut egui::Ui) { // Preview area egui::SidePanel::right("preview_panel") .resizable(true) .default_width(ui.available_width() / 2.0) .show_inside(ui, |ui| { // Preview area with centered content and max width egui::ScrollArea::both() .auto_shrink([false, false]) .id_salt("preview_scroll") .show(ui, |ui| { let max_width = 600; let available_width = ui.available_width(); let content_width = (max_width as f32).min(available_width); let padding = (available_width - content_width) / 2.0; ui.horizontal(|ui| { ui.add_space(padding); ui.vertical(|ui| { ui.set_width(content_width); ui.add_space(15.0); ui.set_min_width(max_width as f32); CommonMarkViewer::new() .default_width(Some(max_width)) .max_image_width(Some(512)) .show(ui, &mut self.preview_cache, &self.content.content); }); }); }); }); } fn editor_ui(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) { let _response = egui::ScrollArea::both() .auto_shrink([false, false]) .id_salt("editor_scroll") .show(ui, |ui| { let max_width = 600; let available_width = ui.available_width(); let content_width = (max_width as f32).min(available_width); let padding = (available_width - content_width).max(30.0) / 2.0; ui.horizontal(|ui| { ui.add_space(padding); ui.vertical(|ui| { ui.set_width(content_width); ui.add_space(15.0); ui.set_min_width(max_width as f32); ui.add( TextEdit::multiline(&mut self.content.content) .id_source("MainEditor_editor") .font(egui::TextStyle::Monospace) .interactive(true) .frame(false) .lock_focus(true) .hint_text("Type here...") .desired_width(max_width as f32), ); }); }); }); } }