use egui::TextEdit; use egui_commonmark::{CommonMarkCache, CommonMarkViewer}; use serde::{self, Deserialize, Serialize}; use std::sync::{Arc, Mutex}; use crate::{ PROJECT_FOLDER, editors::{settings_editor::ProjectSettings, tags::Tag}, llm_integration::content_llm::{ContentAI, ReadyState, ReasoningEffort}, util, }; pub struct MainEditor { pub content: ContentSection, pub show_editor: bool, pub editor_separate_window: bool, pub show_preview: bool, preview_cache: CommonMarkCache, dialog: Option, } 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(), dialog: self.dialog.clone(), } } } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ContentSection { #[serde(default)] pub title: String, #[serde(default)] pub id: String, #[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: uuid::Uuid::new_v4().to_string(), description: String::new(), tags: Vec::new(), content: String::new(), parent: None, saved: false, } } pub fn save(&mut self) -> Result<(), Box> { let path = PROJECT_FOLDER .join("documents") .join(format!("{}.json", &self.id)); let content = serde_json::to_string_pretty(self)?; std::fs::write(path, content)?; self.saved = true; Ok(()) } pub fn load(id: &str) -> Result> { let path = PROJECT_FOLDER.join("documents").join(format!("{id}.json")); let content = std::fs::read_to_string(&path)?; let mut section: Self = serde_json::from_str(&content)?; section.saved = true; section.id = id.to_string(); 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(), dialog: None, } } pub fn open(content: ContentSection) -> Self { Self { content, show_editor: true, show_preview: false, editor_separate_window: false, preview_cache: CommonMarkCache::default(), dialog: None, } } pub fn render_ui(&mut self, project: &mut ProjectSettings, ui: &mut egui::Ui) { if let Some(dialog) = &mut self.dialog { dialog.ui(ui, project); match dialog { ContentAI::Summarise { ready, result, .. } => { if *ready.lock().unwrap() == ReadyState::Ready { self.content.content.push_str(&result.lock().unwrap()); self.content.saved = false; *ready.lock().unwrap() = ReadyState::Idle; } else if *ready.lock().unwrap() == ReadyState::Halted { *ready.lock().unwrap() = ReadyState::Idle; } } ContentAI::Continue { ready, result, content, .. } => { *content = self.content.content.clone(); if *ready.lock().unwrap() == ReadyState::Ready { self.content.content.push_str(&result.lock().unwrap()); self.content.saved = false; *ready.lock().unwrap() = ReadyState::Idle; } else if *ready.lock().unwrap() == ReadyState::Halted { *ready.lock().unwrap() = ReadyState::Idle; } } } } 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() { 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() { eprintln!("Failed to save: {e}"); } } // create copy button if ui.button("Create Copy").clicked() { let mut copy = self.clone(); copy.content.id = uuid::Uuid::new_v4().to_string(); copy.content.title = format!("{} (Copy)", self.content.title); copy.content.save().unwrap(); } // delete button if ui.button("Delete").clicked() { std::fs::remove_file( PROJECT_FOLDER .join("documents") .join(format!("{}.json", self.content.id)), ) .unwrap(); *self = Self::new(); } // revert changes button if ui.button("Revert changes").clicked() { self.content = ContentSection::load(&self.content.id).unwrap(); } // preview toggle ui.checkbox(&mut self.show_preview, "Preview"); // 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(); if self.show_preview { self.preview_ui(ui); } self.editor_ui(ui, project); } pub fn ui(&mut self, ctx: &egui::Context, project: &mut ProjectSettings) { // 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, ui); }); } else { egui::CentralPanel::default().show(ctx, |ui| { self.render_ui(project, 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); let text_edit = 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); let mut ctx_menu = false; let response = ui .add_sized( egui::vec2(max_width as f32 - 30.0, ui.available_height()), text_edit, ) .on_hover_text("Right click to open context menu") .context_menu(|ui| { ctx_menu = true; ui.menu_button("AI Actions", |ui| { ui.add_enabled_ui(project.ai_enabled(), |ui| { if ui.button("Summarise").clicked() { self.dialog = Some(ContentAI::Summarise { result: Arc::new(Mutex::new(String::new())), content: self.content.content.clone(), open: true, ready: Arc::new(Mutex::new(ReadyState::Idle)), }); } if ui.button("Continue").clicked() { self.dialog = Some(ContentAI::Continue { content: self.content.content.clone(), instruction: String::new(), max_tokens: 1024, reasoning_effort: ReasoningEffort::default(), context_override: "".to_string(), result: Arc::new(Mutex::new(String::new())), open: true, ready: Arc::new(Mutex::new(ReadyState::Idle)), temperature: 0.7, model_override: "".to_string(), }); } }); }); }); if let Some(response) = response { if response.response.changed() || ctx_menu { self.content.saved = false; } } }); }); }); } }