Skip to main content

gn_cli/commands/
learning.rs

1use 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    /// Command usage frequencies: "a" -> 42, "l" -> 118, etc.
10    pub command_counts: HashMap<String, u64>,
11    /// Most frequent authors replied to
12    pub favorite_authors: HashMap<String, u64>,
13    /// Most frequently noted file paths
14    pub file_frequencies: HashMap<String, u64>,
15    /// Last active namespace (e.g., "comments", "review")
16    pub preferred_namespace: Option<String>,
17    /// Total commands run
18    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    /// Load or initialize user behavior model
27    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    /// Save state
35    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    /// Learn from a command execution
47    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    /// Smart contextual proactive suggestion based on learned history
63    pub fn suggest_next_action(&self, current_file: Option<&str>) -> Option<String> {
64        // If user frequently reviews or has files they frequently comment on
65        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        // Shortcut suggestion if user types long commands
77        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}