1use anyhow::Result;
4use colored::Colorize;
5use std::io::{self, Write};
6
7use crate::cli::{AnswerCommands, LogCommands, RecentCommands};
8use crate::database;
9
10pub async fn handle(command: LogCommands) -> Result<()> {
12 let db = database::Database::new()?;
13
14 match command {
15 LogCommands::Show { minimal } => show_logs(&db, minimal).await,
16 LogCommands::Recent { command, count } => handle_recent(&db, command, count).await,
17 LogCommands::Current => show_current(&db).await,
18 LogCommands::Stats => show_stats(&db).await,
19 LogCommands::Purge {
20 yes,
21 older_than_days,
22 keep_recent,
23 max_size_mb,
24 } => handle_purge(&db, yes, older_than_days, keep_recent, max_size_mb).await,
25 }
26}
27
28async fn show_logs(db: &database::Database, minimal: bool) -> Result<()> {
29 let entries = db.get_all_logs()?;
30
31 if entries.is_empty() {
32 println!("No chat logs found.");
33 return Ok(());
34 }
35
36 if minimal {
37 use tabled::{Table, Tabled};
38
39 #[derive(Tabled)]
40 struct LogEntry {
41 #[tabled(rename = "Chat ID")]
42 chat_id: String,
43 #[tabled(rename = "Model")]
44 model: String,
45 #[tabled(rename = "Question")]
46 question: String,
47 #[tabled(rename = "Time")]
48 time: String,
49 }
50
51 let table_data: Vec<LogEntry> = entries
52 .into_iter()
53 .map(|entry| LogEntry {
54 chat_id: entry.chat_id[..8].to_string(),
55 model: entry.model,
56 question: if entry.question.len() > 50 {
57 format!("{}...", &entry.question[..50])
58 } else {
59 entry.question
60 },
61 time: entry.timestamp.format("%m-%d %H:%M").to_string(),
62 })
63 .collect();
64
65 let table = Table::new(table_data);
66 println!("{}", table);
67 } else {
68 println!("\n{}", "Chat Logs:".bold().blue());
69
70 for entry in entries {
71 println!(
72 "\n{} {} ({})",
73 "Session:".bold(),
74 &entry.chat_id[..8],
75 entry.timestamp.format("%Y-%m-%d %H:%M:%S")
76 );
77 println!("{} {}", "Model:".bold(), entry.model);
78
79 if let (Some(input_tokens), Some(output_tokens)) =
81 (entry.input_tokens, entry.output_tokens)
82 {
83 println!(
84 "{} {} input + {} output = {} total tokens",
85 "Tokens:".bold(),
86 input_tokens,
87 output_tokens,
88 input_tokens + output_tokens
89 );
90 }
91
92 println!("{} {}", "Q:".yellow(), entry.question);
93 println!(
94 "{} {}",
95 "A:".green(),
96 if entry.response.len() > 200 {
97 format!("{}...", &entry.response[..200])
98 } else {
99 entry.response
100 }
101 );
102 println!("{}", "─".repeat(80).dimmed());
103 }
104 }
105
106 Ok(())
107}
108
109async fn handle_recent(
110 db: &database::Database,
111 command: Option<RecentCommands>,
112 count: usize,
113) -> Result<()> {
114 match command {
115 Some(RecentCommands::Answer { command }) => {
116 let entries = db.get_all_logs()?;
117 if let Some(entry) = entries.first() {
118 match command {
119 Some(AnswerCommands::Code) => {
120 let code_blocks = extract_code_blocks(&entry.response);
121 if code_blocks.is_empty() {
122 anyhow::bail!("No code blocks found in the last answer");
123 } else {
124 for block in code_blocks {
125 println!("{}", block);
126 }
127 }
128 }
129 None => {
130 println!("{}", entry.response);
131 }
132 }
133 } else {
134 anyhow::bail!("No recent logs found");
135 }
136 }
137 Some(RecentCommands::Question) => {
138 let entries = db.get_all_logs()?;
139 if let Some(entry) = entries.first() {
140 println!("{}", entry.question);
141 } else {
142 anyhow::bail!("No recent logs found");
143 }
144 }
145 Some(RecentCommands::Model) => {
146 let entries = db.get_all_logs()?;
147 if let Some(entry) = entries.first() {
148 println!("{}", entry.model);
149 } else {
150 anyhow::bail!("No recent logs found");
151 }
152 }
153 Some(RecentCommands::Session) => {
154 let entries = db.get_all_logs()?;
155 if let Some(entry) = entries.first() {
156 println!("{}", entry.chat_id);
157 } else {
158 anyhow::bail!("No recent logs found");
159 }
160 }
161 None => {
162 let mut entries = db.get_all_logs()?;
164 entries.truncate(count);
165
166 if entries.is_empty() {
167 println!("No recent logs found.");
168 return Ok(());
169 }
170
171 println!(
172 "\n{} (showing {} entries)",
173 "Recent Logs:".bold().blue(),
174 entries.len()
175 );
176
177 for entry in entries {
178 println!(
179 "\n{} {} ({})",
180 "Session:".bold(),
181 &entry.chat_id[..8],
182 entry.timestamp.format("%Y-%m-%d %H:%M:%S")
183 );
184 println!("{} {}", "Model:".bold(), entry.model);
185
186 if let (Some(input_tokens), Some(output_tokens)) =
188 (entry.input_tokens, entry.output_tokens)
189 {
190 println!(
191 "{} {} input + {} output = {} total tokens",
192 "Tokens:".bold(),
193 input_tokens,
194 output_tokens,
195 input_tokens + output_tokens
196 );
197 }
198
199 println!("{} {}", "Q:".yellow(), entry.question);
200 println!(
201 "{} {}",
202 "A:".green(),
203 if entry.response.len() > 150 {
204 format!("{}...", &entry.response[..150])
205 } else {
206 entry.response
207 }
208 );
209 println!("{}", "─".repeat(60).dimmed());
210 }
211 }
212 }
213
214 Ok(())
215}
216
217async fn show_current(db: &database::Database) -> Result<()> {
218 if let Some(session_id) = db.get_current_session_id()? {
219 let history = db.get_chat_history(&session_id)?;
220
221 println!("\n{} {}", "Current Session:".bold().blue(), session_id);
222 println!("{} {} messages", "Messages:".bold(), history.len());
223
224 for (i, entry) in history.iter().enumerate() {
225 println!(
226 "\n{} {} ({})",
227 format!("Message {}:", i + 1).bold(),
228 entry.model,
229 entry.timestamp.format("%H:%M:%S")
230 );
231 println!("{} {}", "Q:".yellow(), entry.question);
232 println!(
233 "{} {}",
234 "A:".green(),
235 if entry.response.len() > 100 {
236 format!("{}...", &entry.response[..100])
237 } else {
238 entry.response.clone()
239 }
240 );
241 }
242 } else {
243 println!("No current session found.");
244 }
245
246 Ok(())
247}
248
249async fn show_stats(db: &database::Database) -> Result<()> {
250 let stats = db.get_stats()?;
251
252 println!("\n{}", "Database Statistics:".bold().blue());
253 println!();
254
255 println!("{} {}", "Total Entries:".bold(), stats.total_entries);
257 println!("{} {}", "Unique Sessions:".bold(), stats.unique_sessions);
258
259 let file_size_str = if stats.file_size_bytes < 1024 {
261 format!("{} bytes", stats.file_size_bytes)
262 } else if stats.file_size_bytes < 1024 * 1024 {
263 format!("{:.1} KB", stats.file_size_bytes as f64 / 1024.0)
264 } else {
265 format!("{:.1} MB", stats.file_size_bytes as f64 / (1024.0 * 1024.0))
266 };
267 println!("{} {}", "Database Size:".bold(), file_size_str);
268
269 if let Some((earliest, latest)) = stats.date_range {
271 println!(
272 "{} {} to {}",
273 "Date Range:".bold(),
274 earliest.format("%Y-%m-%d %H:%M:%S"),
275 latest.format("%Y-%m-%d %H:%M:%S")
276 );
277 } else {
278 println!("{} {}", "Date Range:".bold(), "No entries".dimmed());
279 }
280
281 if !stats.model_usage.is_empty() {
283 println!("\n{}", "Model Usage:".bold().blue());
284 for (model, count) in stats.model_usage {
285 let percentage = if stats.total_entries > 0 {
286 (count as f64 / stats.total_entries as f64) * 100.0
287 } else {
288 0.0
289 };
290 println!(
291 " {} {} ({} - {:.1}%)",
292 "•".blue(),
293 model.bold(),
294 count,
295 percentage
296 );
297 }
298 }
299
300 Ok(())
301}
302
303async fn handle_purge(
304 db: &database::Database,
305 yes: bool,
306 older_than_days: Option<u32>,
307 keep_recent: Option<usize>,
308 max_size_mb: Option<u64>,
309) -> Result<()> {
310 let has_specific_options =
312 older_than_days.is_some() || keep_recent.is_some() || max_size_mb.is_some();
313
314 if has_specific_options {
315 let deleted_count = db.smart_purge(older_than_days, keep_recent, max_size_mb)?;
317
318 if deleted_count > 0 {
319 println!("{} Purged {} log entries", "✓".green(), deleted_count);
320
321 if let Some(days) = older_than_days {
322 println!(" - Removed entries older than {} days", days);
323 }
324 if let Some(count) = keep_recent {
325 println!(" - Kept only the {} most recent entries", count);
326 }
327 if let Some(size) = max_size_mb {
328 println!(" - Enforced maximum database size of {} MB", size);
329 }
330 } else {
331 println!("{} No logs needed to be purged", "ℹ️".blue());
332 }
333 } else {
334 if !yes {
336 print!("Are you sure you want to purge all logs? This cannot be undone. (y/N): ");
337 io::stdout().flush()?;
339
340 let mut input = String::new();
341 io::stdin().read_line(&mut input)?;
342
343 if !input.trim().to_lowercase().starts_with('y') {
344 println!("Purge cancelled.");
345 return Ok(());
346 }
347 }
348
349 db.purge_all_logs()?;
350 println!("{} All logs purged successfully", "✓".green());
351 }
352
353 Ok(())
354}
355
356fn extract_code_blocks(text: &str) -> Vec<String> {
358 let mut code_blocks = Vec::new();
359 let mut in_code_block = false;
360 let mut current_block = String::new();
361
362 for line in text.lines() {
363 if line.starts_with("```") {
364 if in_code_block {
365 if !current_block.trim().is_empty() {
367 code_blocks.push(current_block.trim().to_string());
368 }
369 current_block.clear();
370 in_code_block = false;
371 } else {
372 in_code_block = true;
374 }
375 } else if in_code_block {
376 current_block.push_str(line);
377 current_block.push('\n');
378 }
379 }
380
381 if in_code_block && !current_block.trim().is_empty() {
383 code_blocks.push(current_block.trim().to_string());
384 }
385
386 code_blocks
387}