1use std::path::Path;
6
7pub mod composer;
8
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
11pub struct ChatMessage {
12 pub role: String, pub content: String,
14 pub timestamp: String,
15}
16
17#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct ChatSession {
20 pub messages: Vec<ChatMessage>,
21 pub created_at: String,
22 pub model: Option<String>,
23}
24
25impl ChatSession {
26 pub fn new() -> Self {
28 Self {
29 messages: Vec::new(),
30 created_at: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
31 model: None,
32 }
33 }
34
35 pub fn add_user_message(&mut self, content: &str) {
37 self.messages.push(ChatMessage {
38 role: "user".to_string(),
39 content: content.to_string(),
40 timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
41 });
42 }
43
44 pub fn add_assistant_message(&mut self, content: &str) {
46 self.messages.push(ChatMessage {
47 role: "assistant".to_string(),
48 content: content.to_string(),
49 timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
50 });
51 }
52
53 pub fn history(&self) -> Vec<(&str, &str)> {
55 let mut pairs = Vec::new();
56 let mut current_user: Option<&str> = None;
57
58 for msg in &self.messages {
59 match msg.role.as_str() {
60 "user" => current_user = Some(&msg.content),
61 "assistant" => {
62 if let Some(user_msg) = current_user.take() {
63 pairs.push((user_msg, msg.content.as_str()));
64 }
65 }
66 _ => {}
67 }
68 }
69
70 pairs
71 }
72
73 pub fn last_context(&self, n: usize) -> Vec<&ChatMessage> {
75 self.messages.iter().rev().take(n).rev().collect()
76 }
77
78 pub fn save(&self, path: &Path) -> anyhow::Result<()> {
80 let json = serde_json::to_string_pretty(self)?;
81 std::fs::write(path, json)?;
82 Ok(())
83 }
84
85 pub fn load(path: &Path) -> anyhow::Result<Self> {
87 let json = std::fs::read_to_string(path)?;
88 let session: ChatSession = serde_json::from_str(&json)?;
89 Ok(session)
90 }
91
92 pub fn clear(&mut self) {
94 self.messages.clear();
95 }
96
97 pub fn len(&self) -> usize {
99 self.messages.len()
100 }
101
102 pub fn is_empty(&self) -> bool {
104 self.messages.is_empty()
105 }
106}
107
108impl Default for ChatSession {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_chat_session() {
120 let mut session = ChatSession::new();
121 assert!(session.is_empty());
122
123 session.add_user_message("Hello");
124 session.add_assistant_message("Hi there!");
125 assert_eq!(session.len(), 2);
126
127 let history = session.history();
128 assert_eq!(history.len(), 1);
129 assert_eq!(history[0].0, "Hello");
130 assert_eq!(history[0].1, "Hi there!");
131 }
132
133 #[test]
134 fn test_save_load() {
135 let mut session = ChatSession::new();
136 session.add_user_message("test");
137 session.add_assistant_message("response");
138
139 let tmp = std::env::temp_dir().join("sparrow_test_chat.json");
140 session.save(&tmp).unwrap();
141
142 let loaded = ChatSession::load(&tmp).unwrap();
143 assert_eq!(loaded.len(), 2);
144 assert_eq!(loaded.messages[0].content, "test");
145
146 let _ = std::fs::remove_file(&tmp);
147 }
148}