Skip to main content

gestura_core_analytics/
recommendations.rs

1//! Personalized recommendations for Gestura.app
2//! Provides intelligent recommendations based on user behavior and preferences
3
4#[allow(unused_imports)]
5use gestura_core_foundation::error::AppError;
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10/// Recommendation types
11#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
12pub enum RecommendationType {
13    Feature,
14    Gesture,
15    VoiceCommand,
16    Setting,
17    Workflow,
18    Tutorial,
19    Optimization,
20}
21
22/// Recommendation item
23#[derive(Debug, Clone, serde::Serialize)]
24pub struct Recommendation {
25    pub id: String,
26    pub title: String,
27    pub description: String,
28    pub recommendation_type: RecommendationType,
29    pub confidence: f32,
30    pub priority: u8, // 1-10, 10 being highest
31    pub category: String,
32    pub tags: Vec<String>,
33    pub action_url: Option<String>,
34    pub estimated_benefit: String,
35    pub created_at: chrono::DateTime<chrono::Utc>,
36}
37
38/// User behavior pattern
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
40pub struct UserBehaviorPattern {
41    pub user_id: String,
42    pub feature_usage: HashMap<String, u32>,
43    pub gesture_frequency: HashMap<String, u32>,
44    pub voice_command_frequency: HashMap<String, u32>,
45    pub session_patterns: SessionPatterns,
46    pub error_patterns: HashMap<String, u32>,
47    pub preference_scores: HashMap<String, f32>,
48    pub last_updated: chrono::DateTime<chrono::Utc>,
49}
50
51/// Session usage patterns
52#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
53pub struct SessionPatterns {
54    pub average_session_duration_minutes: f32,
55    pub peak_usage_hours: Vec<u8>,
56    pub most_used_features: Vec<String>,
57    pub feature_adoption_rate: f32,
58    pub error_rate: f32,
59}
60
61impl Default for SessionPatterns {
62    fn default() -> Self {
63        Self {
64            average_session_duration_minutes: 0.0,
65            peak_usage_hours: Vec::new(),
66            most_used_features: Vec::new(),
67            feature_adoption_rate: 0.0,
68            error_rate: 0.0,
69        }
70    }
71}
72
73/// Recommendation engine configuration
74#[derive(Debug, Clone)]
75pub struct RecommendationConfig {
76    pub max_recommendations: usize,
77    pub min_confidence_threshold: f32,
78    pub learning_rate: f32,
79    pub recommendation_refresh_hours: u64,
80    pub enable_cross_user_learning: bool,
81    pub privacy_mode: bool,
82}
83
84impl Default for RecommendationConfig {
85    fn default() -> Self {
86        Self {
87            max_recommendations: 10,
88            min_confidence_threshold: 0.6,
89            learning_rate: 0.1,
90            recommendation_refresh_hours: 24,
91            enable_cross_user_learning: false,
92            privacy_mode: true,
93        }
94    }
95}
96
97/// Personalized recommendation engine
98pub struct PersonalizedRecommendationEngine {
99    user_patterns: Arc<RwLock<HashMap<String, UserBehaviorPattern>>>,
100    recommendations_cache: Arc<RwLock<HashMap<String, Vec<Recommendation>>>>,
101    global_patterns: Arc<RwLock<HashMap<String, f32>>>,
102    config: Arc<RwLock<RecommendationConfig>>,
103    recommendation_templates: Arc<RwLock<Vec<RecommendationTemplate>>>,
104}
105
106/// Recommendation template for generating personalized recommendations
107#[derive(Debug, Clone)]
108struct RecommendationTemplate {
109    #[allow(dead_code)]
110    id: String,
111    title_template: String,
112    description_template: String,
113    recommendation_type: RecommendationType,
114    category: String,
115    tags: Vec<String>,
116    conditions: Vec<RecommendationCondition>,
117    priority: u8,
118    estimated_benefit: String,
119}
120
121/// Conditions for triggering recommendations
122#[derive(Debug, Clone)]
123#[allow(dead_code)]
124enum RecommendationCondition {
125    FeatureUsageBelow { feature: String, threshold: u32 },
126    FeatureUsageAbove { feature: String, threshold: u32 },
127    ErrorRateAbove { threshold: f32 },
128    SessionDurationBelow { threshold_minutes: f32 },
129    GestureAccuracyBelow { threshold: f32 },
130    VoiceAccuracyBelow { threshold: f32 },
131    HasNotUsedFeature { feature: String },
132    UsesFeatureFrequently { feature: String, min_usage: u32 },
133}
134
135impl PersonalizedRecommendationEngine {
136    /// Create a new recommendation engine
137    pub fn new(config: RecommendationConfig) -> Self {
138        let engine = Self {
139            user_patterns: Arc::new(RwLock::new(HashMap::new())),
140            recommendations_cache: Arc::new(RwLock::new(HashMap::new())),
141            global_patterns: Arc::new(RwLock::new(HashMap::new())),
142            config: Arc::new(RwLock::new(config)),
143            recommendation_templates: Arc::new(RwLock::new(Vec::new())),
144        };
145
146        // Initialize with default templates
147        tokio::spawn({
148            let engine = engine.clone();
149            async move {
150                if let Err(e) = engine.initialize_default_templates().await {
151                    tracing::error!("Failed to initialize recommendation templates: {}", e);
152                }
153            }
154        });
155
156        engine
157    }
158
159    /// Update user behavior pattern
160    pub async fn update_user_pattern(
161        &self,
162        user_id: &str,
163        usage_data: serde_json::Value,
164    ) -> Result<(), AppError> {
165        let mut patterns = self.user_patterns.write().await;
166
167        let pattern = patterns
168            .entry(user_id.to_string())
169            .or_insert_with(|| UserBehaviorPattern {
170                user_id: user_id.to_string(),
171                feature_usage: HashMap::new(),
172                gesture_frequency: HashMap::new(),
173                voice_command_frequency: HashMap::new(),
174                session_patterns: SessionPatterns::default(),
175                error_patterns: HashMap::new(),
176                preference_scores: HashMap::new(),
177                last_updated: chrono::Utc::now(),
178            });
179
180        // Update pattern based on usage data
181        if let Some(features) = usage_data.get("features")
182            && let Ok(feature_map) =
183                serde_json::from_value::<HashMap<String, u32>>(features.clone())
184        {
185            for (feature, count) in feature_map {
186                *pattern.feature_usage.entry(feature).or_insert(0) += count;
187            }
188        }
189
190        if let Some(gestures) = usage_data.get("gestures")
191            && let Ok(gesture_map) =
192                serde_json::from_value::<HashMap<String, u32>>(gestures.clone())
193        {
194            for (gesture, count) in gesture_map {
195                *pattern.gesture_frequency.entry(gesture).or_insert(0) += count;
196            }
197        }
198
199        if let Some(voice_commands) = usage_data.get("voice_commands")
200            && let Ok(voice_map) =
201                serde_json::from_value::<HashMap<String, u32>>(voice_commands.clone())
202        {
203            for (command, count) in voice_map {
204                *pattern.voice_command_frequency.entry(command).or_insert(0) += count;
205            }
206        }
207
208        pattern.last_updated = chrono::Utc::now();
209
210        // Clear cached recommendations for this user
211        let mut cache = self.recommendations_cache.write().await;
212        cache.remove(user_id);
213
214        tracing::debug!("Updated behavior pattern for user: {}", user_id);
215        Ok(())
216    }
217
218    /// Generate personalized recommendations for a user
219    pub async fn generate_recommendations(
220        &self,
221        user_id: &str,
222    ) -> Result<Vec<Recommendation>, AppError> {
223        // Check cache first
224        {
225            let cache = self.recommendations_cache.read().await;
226            if let Some(cached_recommendations) = cache.get(user_id) {
227                let config = self.config.read().await;
228                let cache_age = chrono::Utc::now().timestamp()
229                    - cached_recommendations
230                        .first()
231                        .map(|r| r.created_at.timestamp())
232                        .unwrap_or(0);
233
234                if cache_age < (config.recommendation_refresh_hours * 3600) as i64 {
235                    return Ok(cached_recommendations.clone());
236                }
237            }
238        }
239
240        let patterns = self.user_patterns.read().await;
241        let user_pattern = patterns.get(user_id);
242
243        if user_pattern.is_none() {
244            return Ok(Vec::new()); // No data yet
245        }
246
247        let user_pattern = user_pattern.unwrap();
248        let templates = self.recommendation_templates.read().await;
249        let mut recommendations = Vec::new();
250
251        // Generate recommendations based on templates
252        for template in templates.iter() {
253            if let Some(recommendation) = self.evaluate_template(template, user_pattern).await {
254                recommendations.push(recommendation);
255            }
256        }
257
258        // Sort by priority and confidence
259        recommendations.sort_by(|a, b| {
260            b.priority.cmp(&a.priority).then_with(|| {
261                b.confidence
262                    .partial_cmp(&a.confidence)
263                    .unwrap_or(std::cmp::Ordering::Equal)
264            })
265        });
266
267        // Apply filters
268        let config = self.config.read().await;
269        recommendations.retain(|r| r.confidence >= config.min_confidence_threshold);
270        recommendations.truncate(config.max_recommendations);
271
272        // Cache recommendations
273        drop(config);
274        drop(templates);
275        drop(patterns);
276        let mut cache = self.recommendations_cache.write().await;
277        cache.insert(user_id.to_string(), recommendations.clone());
278
279        tracing::info!(
280            "Generated {} recommendations for user: {}",
281            recommendations.len(),
282            user_id
283        );
284        Ok(recommendations)
285    }
286
287    /// Evaluate a recommendation template against user pattern
288    async fn evaluate_template(
289        &self,
290        template: &RecommendationTemplate,
291        user_pattern: &UserBehaviorPattern,
292    ) -> Option<Recommendation> {
293        let mut confidence = 0.5; // Base confidence
294        let mut conditions_met = 0;
295        let total_conditions = template.conditions.len();
296
297        // Evaluate conditions
298        for condition in &template.conditions {
299            match condition {
300                RecommendationCondition::FeatureUsageBelow { feature, threshold } => {
301                    let usage = user_pattern.feature_usage.get(feature).unwrap_or(&0);
302                    if *usage < *threshold {
303                        conditions_met += 1;
304                        confidence += 0.1;
305                    }
306                }
307                RecommendationCondition::FeatureUsageAbove { feature, threshold } => {
308                    let usage = user_pattern.feature_usage.get(feature).unwrap_or(&0);
309                    if *usage > *threshold {
310                        conditions_met += 1;
311                        confidence += 0.1;
312                    }
313                }
314                RecommendationCondition::ErrorRateAbove { threshold }
315                    if user_pattern.session_patterns.error_rate > *threshold =>
316                {
317                    conditions_met += 1;
318                    confidence += 0.15;
319                }
320                RecommendationCondition::SessionDurationBelow { threshold_minutes }
321                    if user_pattern
322                        .session_patterns
323                        .average_session_duration_minutes
324                        < *threshold_minutes =>
325                {
326                    conditions_met += 1;
327                    confidence += 0.1;
328                }
329                RecommendationCondition::HasNotUsedFeature { feature }
330                    if !user_pattern.feature_usage.contains_key(feature) =>
331                {
332                    conditions_met += 1;
333                    confidence += 0.2;
334                }
335                RecommendationCondition::UsesFeatureFrequently { feature, min_usage } => {
336                    let usage = user_pattern.feature_usage.get(feature).unwrap_or(&0);
337                    if *usage >= *min_usage {
338                        conditions_met += 1;
339                        confidence += 0.1;
340                    }
341                }
342                _ => {} // Handle other conditions
343            }
344        }
345
346        // Require at least 50% of conditions to be met
347        if total_conditions > 0 && (conditions_met as f32 / total_conditions as f32) < 0.5 {
348            return None;
349        }
350
351        // Adjust confidence based on user preferences
352        if let Some(pref_score) = user_pattern.preference_scores.get(&template.category) {
353            confidence *= pref_score;
354        }
355
356        Some(Recommendation {
357            id: uuid::Uuid::new_v4().to_string(),
358            title: self.personalize_text(&template.title_template, user_pattern),
359            description: self.personalize_text(&template.description_template, user_pattern),
360            recommendation_type: template.recommendation_type.clone(),
361            confidence: confidence.min(1.0),
362            priority: template.priority,
363            category: template.category.clone(),
364            tags: template.tags.clone(),
365            action_url: None,
366            estimated_benefit: template.estimated_benefit.clone(),
367            created_at: chrono::Utc::now(),
368        })
369    }
370
371    /// Personalize text templates with user data
372    fn personalize_text(&self, template: &str, user_pattern: &UserBehaviorPattern) -> String {
373        let mut result = template.to_string();
374
375        // Replace placeholders with user-specific data
376        if let Some(most_used) = user_pattern.session_patterns.most_used_features.first() {
377            result = result.replace("{most_used_feature}", most_used);
378        }
379
380        result = result.replace(
381            "{session_duration}",
382            &format!(
383                "{:.1}",
384                user_pattern
385                    .session_patterns
386                    .average_session_duration_minutes
387            ),
388        );
389
390        result = result.replace(
391            "{error_rate}",
392            &format!("{:.1}%", user_pattern.session_patterns.error_rate * 100.0),
393        );
394
395        result
396    }
397
398    /// Initialize default recommendation templates
399    async fn initialize_default_templates(&self) -> Result<(), AppError> {
400        let mut templates = self.recommendation_templates.write().await;
401
402        templates.push(RecommendationTemplate {
403            id: "voice_training".to_string(),
404            title_template: "Improve Voice Recognition Accuracy".to_string(),
405            description_template: "Your voice recognition accuracy could be improved. Try the voice training feature to personalize the system to your voice.".to_string(),
406            recommendation_type: RecommendationType::Feature,
407            category: "voice".to_string(),
408            tags: vec!["accuracy".to_string(), "training".to_string()],
409            conditions: vec![
410                RecommendationCondition::HasNotUsedFeature { feature: "voice_training".to_string() },
411                RecommendationCondition::ErrorRateAbove { threshold: 0.1 },
412            ],
413            priority: 8,
414            estimated_benefit: "Up to 30% improvement in voice recognition accuracy".to_string(),
415        });
416
417        templates.push(RecommendationTemplate {
418            id: "gesture_customization".to_string(),
419            title_template: "Customize Your Gestures".to_string(),
420            description_template: "You use gestures frequently! Create custom gestures for your most common actions to save time.".to_string(),
421            recommendation_type: RecommendationType::Feature,
422            category: "gestures".to_string(),
423            tags: vec!["customization".to_string(), "efficiency".to_string()],
424            conditions: vec![
425                RecommendationCondition::UsesFeatureFrequently { feature: "gestures".to_string(), min_usage: 50 },
426                RecommendationCondition::HasNotUsedFeature { feature: "custom_gestures".to_string() },
427            ],
428            priority: 7,
429            estimated_benefit: "Reduce gesture time by up to 40%".to_string(),
430        });
431
432        templates.push(RecommendationTemplate {
433            id: "session_optimization".to_string(),
434            title_template: "Optimize Your Workflow".to_string(),
435            description_template: "Your sessions are shorter than average ({session_duration} min). Try these workflow optimizations to be more productive.".to_string(),
436            recommendation_type: RecommendationType::Optimization,
437            category: "productivity".to_string(),
438            tags: vec!["workflow".to_string(), "efficiency".to_string()],
439            conditions: vec![
440                RecommendationCondition::SessionDurationBelow { threshold_minutes: 15.0 },
441            ],
442            priority: 6,
443            estimated_benefit: "Increase productivity by 25%".to_string(),
444        });
445
446        templates.push(RecommendationTemplate {
447            id: "ring_calibration".to_string(),
448            title_template: "Calibrate Your Ring".to_string(),
449            description_template: "Your gesture accuracy seems low. Try recalibrating your Haptic Harmony ring for better performance.".to_string(),
450            recommendation_type: RecommendationType::Setting,
451            category: "hardware".to_string(),
452            tags: vec!["calibration".to_string(), "accuracy".to_string()],
453            conditions: vec![
454                RecommendationCondition::GestureAccuracyBelow { threshold: 0.8 },
455            ],
456            priority: 9,
457            estimated_benefit: "Improve gesture accuracy by up to 50%".to_string(),
458        });
459
460        templates.push(RecommendationTemplate {
461            id: "tutorial_advanced".to_string(),
462            title_template: "Learn Advanced Features".to_string(),
463            description_template: "You're using {most_used_feature} frequently. Learn about advanced features that can enhance your experience.".to_string(),
464            recommendation_type: RecommendationType::Tutorial,
465            category: "learning".to_string(),
466            tags: vec!["advanced".to_string(), "features".to_string()],
467            conditions: vec![
468                RecommendationCondition::FeatureUsageAbove { feature: "basic_features".to_string(), threshold: 100 },
469            ],
470            priority: 5,
471            estimated_benefit: "Unlock 10+ advanced features".to_string(),
472        });
473
474        tracing::info!("Initialized {} recommendation templates", templates.len());
475        Ok(())
476    }
477
478    /// Record user feedback on recommendations
479    pub async fn record_feedback(
480        &self,
481        user_id: &str,
482        recommendation_id: &str,
483        feedback: RecommendationFeedback,
484    ) -> Result<(), AppError> {
485        let mut patterns = self.user_patterns.write().await;
486
487        if let Some(pattern) = patterns.get_mut(user_id) {
488            // Update preference scores based on feedback
489            let cache = self.recommendations_cache.read().await;
490            if let Some(recommendations) = cache.get(user_id)
491                && let Some(recommendation) =
492                    recommendations.iter().find(|r| r.id == recommendation_id)
493            {
494                let category = &recommendation.category;
495                let current_score = pattern.preference_scores.get(category).unwrap_or(&0.5);
496
497                let adjustment = match feedback {
498                    RecommendationFeedback::Helpful => 0.1,
499                    RecommendationFeedback::NotHelpful => -0.1,
500                    RecommendationFeedback::Implemented => 0.2,
501                    RecommendationFeedback::Dismissed => -0.05,
502                };
503
504                let new_score = (current_score + adjustment).clamp(0.0, 1.0);
505                pattern
506                    .preference_scores
507                    .insert(category.clone(), new_score);
508
509                tracing::debug!(
510                    "Updated preference score for category '{}' to {:.2} based on feedback",
511                    category,
512                    new_score
513                );
514            }
515        }
516
517        Ok(())
518    }
519
520    /// Get recommendation statistics
521    pub async fn get_stats(&self) -> serde_json::Value {
522        let patterns = self.user_patterns.read().await;
523        let cache = self.recommendations_cache.read().await;
524        let templates = self.recommendation_templates.read().await;
525
526        let total_users = patterns.len();
527        let total_cached_recommendations: usize = cache.values().map(|v| v.len()).sum();
528        let total_templates = templates.len();
529
530        serde_json::json!({
531            "total_users": total_users,
532            "total_cached_recommendations": total_cached_recommendations,
533            "total_templates": total_templates,
534            "average_recommendations_per_user": if total_users > 0 {
535                total_cached_recommendations as f64 / total_users as f64
536            } else {
537                0.0
538            }
539        })
540    }
541
542    /// Clear user data
543    pub async fn clear_user_data(&self, user_id: &str) -> Result<(), AppError> {
544        let mut patterns = self.user_patterns.write().await;
545        let mut cache = self.recommendations_cache.write().await;
546
547        patterns.remove(user_id);
548        cache.remove(user_id);
549
550        tracing::info!("Cleared recommendation data for user: {}", user_id);
551        Ok(())
552    }
553}
554
555impl Clone for PersonalizedRecommendationEngine {
556    fn clone(&self) -> Self {
557        Self {
558            user_patterns: self.user_patterns.clone(),
559            recommendations_cache: self.recommendations_cache.clone(),
560            global_patterns: self.global_patterns.clone(),
561            config: self.config.clone(),
562            recommendation_templates: self.recommendation_templates.clone(),
563        }
564    }
565}
566
567/// User feedback on recommendations
568#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
569pub enum RecommendationFeedback {
570    Helpful,
571    NotHelpful,
572    Implemented,
573    Dismissed,
574}
575
576/// Global recommendation engine instance
577static RECOMMENDATION_ENGINE: tokio::sync::OnceCell<PersonalizedRecommendationEngine> =
578    tokio::sync::OnceCell::const_new();
579
580/// Get the global recommendation engine
581pub async fn get_recommendation_engine() -> &'static PersonalizedRecommendationEngine {
582    RECOMMENDATION_ENGINE
583        .get_or_init(|| async {
584            PersonalizedRecommendationEngine::new(RecommendationConfig::default())
585        })
586        .await
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592
593    #[tokio::test]
594    async fn test_recommendation_generation() {
595        let engine = PersonalizedRecommendationEngine::new(RecommendationConfig::default());
596
597        // Wait a bit for background template initialization to complete
598        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
599
600        // Create user pattern
601        let usage_data = serde_json::json!({
602            "features": {
603                "voice_commands": 10,
604                "gestures": 50
605            },
606            "gestures": {
607                "tap": 30,
608                "swipe": 20
609            }
610        });
611
612        engine
613            .update_user_pattern("user1", usage_data)
614            .await
615            .unwrap();
616
617        let recommendations = engine.generate_recommendations("user1").await.unwrap();
618        assert!(!recommendations.is_empty());
619    }
620
621    #[tokio::test]
622    async fn test_feedback_recording() {
623        let engine = PersonalizedRecommendationEngine::new(RecommendationConfig::default());
624
625        // Setup user and generate recommendations
626        let usage_data = serde_json::json!({
627            "features": {"gestures": 100}
628        });
629
630        engine
631            .update_user_pattern("user1", usage_data)
632            .await
633            .unwrap();
634        let recommendations = engine.generate_recommendations("user1").await.unwrap();
635
636        if let Some(rec) = recommendations.first() {
637            engine
638                .record_feedback("user1", &rec.id, RecommendationFeedback::Helpful)
639                .await
640                .unwrap();
641        }
642
643        // Verify feedback was recorded (would check preference scores in real test)
644        let stats = engine.get_stats().await;
645        assert_eq!(stats["total_users"].as_u64().unwrap(), 1);
646    }
647}