gn_cli/commands/
learning.rs1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Serialize, Deserialize, Default, Debug)]
8pub struct UserBehaviorProfile {
9 pub command_counts: HashMap<String, u64>,
11 pub favorite_authors: HashMap<String, u64>,
13 pub file_frequencies: HashMap<String, u64>,
15 pub preferred_namespace: Option<String>,
17 pub total_interactions: u64,
19}
20
21impl UserBehaviorProfile {
22 fn profile_path() -> Option<PathBuf> {
23 dirs_fallback().map(|p| p.join("profile.json"))
24 }
25
26 pub fn load() -> Self {
28 Self::profile_path()
29 .and_then(|p| fs::read_to_string(p).ok())
30 .and_then(|s| serde_json::from_str(&s).ok())
31 .unwrap_or_default()
32 }
33
34 pub fn save(&self) -> Result<()> {
36 if let Some(path) = Self::profile_path() {
37 if let Some(parent) = path.parent() {
38 let _ = fs::create_dir_all(parent);
39 }
40 let data = serde_json::to_string_pretty(self)?;
41 fs::write(path, data)?;
42 }
43 Ok(())
44 }
45
46 pub fn record_interaction(&mut self, cmd: &str, file: Option<&str>, ns: Option<&str>) {
48 *self.command_counts.entry(cmd.to_string()).or_insert(0) += 1;
49 self.total_interactions += 1;
50
51 if let Some(f) = file {
52 *self.file_frequencies.entry(f.to_string()).or_insert(0) += 1;
53 }
54
55 if let Some(n) = ns {
56 self.preferred_namespace = Some(n.to_string());
57 }
58
59 let _ = self.save();
60 }
61
62 pub fn suggest_next_action(&self, current_file: Option<&str>) -> Option<String> {
64 if let Some(f) = current_file {
66 if let Some(count) = self.file_frequencies.get(f) {
67 if *count > 3 {
68 return Some(format!(
69 "\x1b[90m💡 Pro-tip: You frequently annotate '{}'. Run \x1b[36mgn d\x1b[90m to view inline diff notes.\x1b[0m",
70 f
71 ));
72 }
73 }
74 }
75
76 let list_count = self.command_counts.get("list").copied().unwrap_or(0);
78 let l_count = self.command_counts.get("l").copied().unwrap_or(0);
79 if list_count > 3 && l_count == 0 {
80 return Some("\x1b[90m💡 Shortcut tip: Type \x1b[36mgn l\x1b[90m instead of 'git-notes list' to save keystrokes.\x1b[0m".to_string());
81 }
82
83 None
84 }
85}
86
87fn dirs_fallback() -> Option<PathBuf> {
88 std::env::var_os("USERPROFILE")
89 .or_else(|| std::env::var_os("HOME"))
90 .map(|h| PathBuf::from(h).join(".git-notes"))
91}