use crate::core::message::{Message, ROLE_APP_LOG, ROLE_ASSISTANT, ROLE_USER};
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
use std::path::Path;
use tempfile::NamedTempFile;
pub struct LoggingState {
file_path: Option<String>,
is_active: bool,
}
impl LoggingState {
pub fn new(log_file: Option<String>) -> Result<Self, Box<dyn std::error::Error>> {
let logging = LoggingState {
file_path: log_file,
is_active: false,
};
Ok(logging)
}
pub fn set_log_file(&mut self, path: String) -> Result<String, Box<dyn std::error::Error>> {
self.test_file_access(&path)?;
self.file_path = Some(path.clone());
self.is_active = true;
Ok(format!("Logging enabled to: {path}"))
}
pub fn toggle_logging(
&mut self,
pause_message: &str,
) -> Result<String, Box<dyn std::error::Error>> {
match &self.file_path {
Some(path) => {
if self.is_active {
self.log_message(&format!("## {}", pause_message))?;
self.is_active = false;
Ok(format!("Logging paused (file: {path})"))
} else {
self.is_active = true;
Ok(format!("Logging resumed to: {path}"))
}
}
None => {
Err("No log file specified. Use /log <filename> to enable logging first.".into())
}
}
}
pub fn log_message(&self, content: &str) -> Result<(), Box<dyn std::error::Error>> {
if !self.is_active || self.file_path.is_none() {
return Ok(());
}
self.write_to_log(content)
}
fn write_to_log(&self, content: &str) -> Result<(), Box<dyn std::error::Error>> {
let file_path = self.file_path.as_ref().unwrap();
let file = OpenOptions::new()
.create(true)
.append(true)
.open(file_path)?;
let mut writer = BufWriter::with_capacity(64 * 1024, file);
for line in content.lines() {
writeln!(writer, "{line}")?;
}
writeln!(writer)?;
writer.flush()?;
Ok(())
}
pub fn is_active(&self) -> bool {
self.is_active
}
pub fn get_status_string(&self) -> String {
match (&self.file_path, self.is_active) {
(None, _) => "disabled".to_string(),
(Some(path), true) => format!(
"active ({})",
Path::new(path)
.file_name()
.unwrap_or_default()
.to_string_lossy()
),
(Some(path), false) => format!(
"paused ({})",
Path::new(path)
.file_name()
.unwrap_or_default()
.to_string_lossy()
),
}
}
pub fn rewrite_log_without_last_response(
&self,
messages: &std::collections::VecDeque<Message>,
user_display_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
self.rewrite_log_skip_index(messages, user_display_name, None)
}
pub fn rewrite_log_skip_index(
&self,
messages: &std::collections::VecDeque<Message>,
user_display_name: &str,
skip_index: Option<usize>,
) -> Result<(), Box<dyn std::error::Error>> {
if !self.is_active || self.file_path.is_none() {
return Ok(());
}
let file_path = self.file_path.as_ref().unwrap();
let target_path = Path::new(file_path);
let parent = target_path.parent().unwrap_or_else(|| Path::new("."));
let mut temp_file = NamedTempFile::new_in(parent)?;
for (i, msg) in messages.iter().enumerate() {
if Some(i) == skip_index {
continue;
}
if msg.role == ROLE_USER {
for line in format!("{}: {}", user_display_name, msg.content).lines() {
writeln!(temp_file, "{line}")?;
}
writeln!(temp_file)?; } else if msg.role == ROLE_ASSISTANT && !msg.content.is_empty() {
for line in msg.content.lines() {
writeln!(temp_file, "{line}")?;
}
writeln!(temp_file)?; } else if msg.role == ROLE_APP_LOG {
for line in format!("## {}", msg.content).lines() {
writeln!(temp_file, "{line}")?;
}
writeln!(temp_file)?; }
}
temp_file.flush()?;
temp_file.as_file().sync_all()?;
temp_file.persist(file_path)?;
Ok(())
}
fn test_file_access(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
file.flush()?;
Ok(())
}
}