From 735ae69dbd99867e44becdea3ca961bc6d1be50c Mon Sep 17 00:00:00 2001 From: leach Date: Tue, 19 Aug 2025 00:45:34 -0400 Subject: [PATCH] export functionality and session management --- .gitignore | 4 ++ src/cli.rs | 114 +++++++++++++++++++++++++++++++++++++++++++ src/core/session.rs | 110 +++++++++++++++++++++++++++++++++++++++++ src/utils/display.rs | 2 + 4 files changed, 230 insertions(+) diff --git a/.gitignore b/.gitignore index a2ff8b1..a28de04 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ Thumbs.db # Backup files *.bak *.backup + +# Random +exports/ +improvements.txt diff --git a/src/cli.rs b/src/cli.rs index 5203780..d5e40bb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -201,6 +201,12 @@ impl ChatCLI { "/history" => { self.handle_history_command(&parts)?; } + "/export" => { + self.handle_export_command(&parts)?; + } + "/save" => { + self.handle_save_command(&parts)?; + } _ => { self.display.print_error(&format!("Unknown command: {} (see /help)", parts[0])); } @@ -490,4 +496,112 @@ impl ChatCLI { Ok(()) } + fn handle_export_command(&mut self, parts: &[&str]) -> Result<()> { + let format = if parts.len() > 1 { + parts[1].to_lowercase() + } else { + // Default to markdown + "markdown".to_string() + }; + + let valid_formats = ["markdown", "md", "json", "txt"]; + if !valid_formats.contains(&format.as_str()) { + self.display.print_error(&format!( + "Invalid format '{}'. Supported formats: markdown, json, txt", + format + )); + self.display.print_info("Usage: /export [format]"); + return Ok(()); + } + + // Generate filename with timestamp + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + let extension = match format.as_str() { + "markdown" | "md" => "md", + "json" => "json", + "txt" => "txt", + _ => "md", + }; + + // Create exports directory if it doesn't exist + let exports_dir = std::path::Path::new("exports"); + if !exports_dir.exists() { + if let Err(e) = std::fs::create_dir(exports_dir) { + self.display.print_error(&format!("Failed to create exports directory: {}", e)); + return Ok(()); + } + } + + let filename = format!("{}_{}.{}", self.session.name, now, extension); + let file_path = exports_dir.join(&filename); + + match self.session.export(&format, file_path.to_str().unwrap_or(&filename)) { + Ok(()) => { + self.display.print_command_result(&format!( + "Conversation exported to '{}'", + file_path.display() + )); + } + Err(e) => { + self.display.print_error(&format!("Export failed: {}", e)); + } + } + + Ok(()) + } + + fn handle_save_command(&mut self, parts: &[&str]) -> Result<()> { + if parts.len() != 2 { + self.display.print_error("Usage: /save "); + self.display.print_info("Example: /save my_important_conversation"); + return Ok(()); + } + + let new_session_name = parts[1]; + + // Validate session name + if new_session_name.is_empty() { + self.display.print_error("Session name cannot be empty"); + return Ok(()); + } + + // Check if session already exists + if let Ok(sessions) = Session::list_sessions() { + if sessions.iter().any(|(name, _)| name == new_session_name) { + self.display.print_error(&format!( + "Session '{}' already exists. Choose a different name.", + new_session_name + )); + return Ok(()); + } + } + + // Save the current session first to ensure it's up to date + self.session.save()?; + + // Copy the session with the new name + match self.session.save_as(new_session_name) { + Ok(()) => { + self.display.print_command_result(&format!( + "Current session saved as '{}' ({} messages copied)", + new_session_name, + self.session.messages.len().saturating_sub(1) // Exclude system prompt + )); + self.display.print_info(&format!( + "Use '/switch' to switch to the new session, or continue with current session '{}'", + self.session.name + )); + } + Err(e) => { + self.display.print_error(&format!("Failed to save session: {}", e)); + } + } + + Ok(()) + } + } \ No newline at end of file diff --git a/src/core/session.rs b/src/core/session.rs index 875dd4b..d425957 100644 --- a/src/core/session.rs +++ b/src/core/session.rs @@ -376,4 +376,114 @@ impl Session { Ok(()) } + + pub fn save_as(&self, new_name: &str) -> Result<()> { + // Create a new session with the same data but different name + let new_session = Session { + name: new_name.to_string(), + model: self.model.clone(), + messages: self.messages.clone(), + enable_web_search: self.enable_web_search, + enable_reasoning_summary: self.enable_reasoning_summary, + reasoning_effort: self.reasoning_effort.clone(), + }; + + // Save the new session + new_session.save() + .with_context(|| format!("Failed to save session as '{}'", new_name)) + } + + pub fn export(&self, format: &str, filename: &str) -> Result<()> { + let content = match format { + "markdown" | "md" => self.export_markdown(), + "json" => self.export_json()?, + "txt" => self.export_text(), + _ => return Err(anyhow::anyhow!("Unsupported export format: {}", format)), + }; + + std::fs::write(filename, content) + .with_context(|| format!("Failed to write export file: {}", filename))?; + + Ok(()) + } + + fn export_markdown(&self) -> String { + let mut content = String::new(); + + // Header + content.push_str(&format!("# Conversation: {}\n\n", self.name)); + content.push_str(&format!("**Model:** {}\n", self.model)); + content.push_str(&format!("**Web Search:** {}\n", if self.enable_web_search { "Enabled" } else { "Disabled" })); + content.push_str(&format!("**Reasoning Summary:** {}\n", if self.enable_reasoning_summary { "Enabled" } else { "Disabled" })); + content.push_str(&format!("**Reasoning Effort:** {}\n\n", self.reasoning_effort)); + content.push_str("---\n\n"); + + // Messages (skip system prompt) + for message in self.messages.iter().skip(1) { + match message.role.as_str() { + "user" => { + content.push_str("## 👤 User\n\n"); + content.push_str(&message.content); + content.push_str("\n\n"); + } + "assistant" => { + content.push_str("## 🤖 Assistant\n\n"); + content.push_str(&message.content); + content.push_str("\n\n"); + } + _ => {} + } + } + + content + } + + fn export_json(&self) -> Result { + let export_data = serde_json::json!({ + "session_name": self.name, + "model": self.model, + "settings": { + "enable_web_search": self.enable_web_search, + "enable_reasoning_summary": self.enable_reasoning_summary, + "reasoning_effort": self.reasoning_effort + }, + "messages": self.messages, + "exported_at": chrono::Utc::now().to_rfc3339() + }); + + serde_json::to_string_pretty(&export_data) + .with_context(|| "Failed to serialize conversation to JSON") + } + + fn export_text(&self) -> String { + let mut content = String::new(); + + // Header + content.push_str(&format!("Conversation: {}\n", self.name)); + content.push_str(&format!("Model: {}\n", self.model)); + content.push_str(&format!("Web Search: {}\n", if self.enable_web_search { "Enabled" } else { "Disabled" })); + content.push_str(&format!("Reasoning Summary: {}\n", if self.enable_reasoning_summary { "Enabled" } else { "Disabled" })); + content.push_str(&format!("Reasoning Effort: {}\n\n", self.reasoning_effort)); + content.push_str("===============================================\n\n"); + + // Messages (skip system prompt) + for message in self.messages.iter().skip(1) { + match message.role.as_str() { + "user" => { + content.push_str("USER:\n"); + content.push_str(&message.content); + content.push_str("\n\n"); + } + "assistant" => { + content.push_str("ASSISTANT:\n"); + content.push_str(&message.content); + content.push_str("\n\n"); + } + _ => {} + } + content.push_str("-----------------------------------------------\n\n"); + } + + content + } } \ No newline at end of file diff --git a/src/utils/display.rs b/src/utils/display.rs index 09c060b..8c56d16 100644 --- a/src/utils/display.rs +++ b/src/utils/display.rs @@ -192,6 +192,8 @@ Available Commands: /switch - Interactive session manager (switch/delete) /clear - Clear current conversation /history [user|assistant] [number] - View conversation history + /export [format] - Export conversation to exports/ (markdown, json, txt) + /save - Save current session with a new name /tools - Interactive tool and feature manager Input Features: