export functionality and session management
This commit is contained in:
parent
0faecbf657
commit
735ae69dbd
|
|
@ -42,3 +42,7 @@ Thumbs.db
|
||||||
# Backup files
|
# Backup files
|
||||||
*.bak
|
*.bak
|
||||||
*.backup
|
*.backup
|
||||||
|
|
||||||
|
# Random
|
||||||
|
exports/
|
||||||
|
improvements.txt
|
||||||
|
|
|
||||||
114
src/cli.rs
114
src/cli.rs
|
|
@ -201,6 +201,12 @@ impl ChatCLI {
|
||||||
"/history" => {
|
"/history" => {
|
||||||
self.handle_history_command(&parts)?;
|
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]));
|
self.display.print_error(&format!("Unknown command: {} (see /help)", parts[0]));
|
||||||
}
|
}
|
||||||
|
|
@ -490,4 +496,112 @@ impl ChatCLI {
|
||||||
Ok(())
|
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 <new_session_name>");
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -376,4 +376,114 @@ impl Session {
|
||||||
|
|
||||||
Ok(())
|
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<String> {
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -192,6 +192,8 @@ Available Commands:
|
||||||
/switch - Interactive session manager (switch/delete)
|
/switch - Interactive session manager (switch/delete)
|
||||||
/clear - Clear current conversation
|
/clear - Clear current conversation
|
||||||
/history [user|assistant] [number] - View conversation history
|
/history [user|assistant] [number] - View conversation history
|
||||||
|
/export [format] - Export conversation to exports/ (markdown, json, txt)
|
||||||
|
/save <new_name> - Save current session with a new name
|
||||||
/tools - Interactive tool and feature manager
|
/tools - Interactive tool and feature manager
|
||||||
|
|
||||||
Input Features:
|
Input Features:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue