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                        conditions_met += 1;
317                        confidence += 0.15;
318                    }
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                }
330                RecommendationCondition::HasNotUsedFeature { feature } => {
331                    if !user_pattern.feature_usage.contains_key(feature) {
332                        conditions_met += 1;
333                        confidence += 0.2;
334                    }
335                }
336                RecommendationCondition::UsesFeatureFrequently { feature, min_usage } => {
337                    let usage = user_pattern.feature_usage.get(feature).unwrap_or(&0);
338                    if *usage >= *min_usage {
339                        conditions_met += 1;
340                        confidence += 0.1;
341                    }
342                }
343                _ => {} // Handle other conditions
344            }
345        }
346
347        // Require at least 50% of conditions to be met
348        if total_conditions > 0 && (conditions_met as f32 / total_conditions as f32) < 0.5 {
349            return None;
350        }
351
352        // Adjust confidence based on user preferences
353        if let Some(pref_score) = user_pattern.preference_scores.get(&template.category) {
354            confidence *= pref_score;
355        }
356
357        Some(Recommendation {
358            id: uuid::Uuid::new_v4().to_string(),
359            title: self.personalize_text(&template.title_template, user_pattern),
360            description: self.personalize_text(&template.description_template, user_pattern),
361            recommendation_type: template.recommendation_type.clone(),
362            confidence: confidence.min(1.0),
363            priority: template.priority,
364            category: template.category.clone(),
365            tags: template.tags.clone(),
366            action_url: None,
367            estimated_benefit: template.estimated_benefit.clone(),
368            created_at: chrono::Utc::now(),
369        })
370    }
371
372    /// Personalize text templates with user data
373    fn personalize_text(&self, template: &str, user_pattern: &UserBehaviorPattern) -> String {
374        let mut result = template.to_string();
375
376        // Replace placeholders with user-specific data
377        if let Some(most_used) = user_pattern.session_patterns.most_used_features.first() {
378            result = result.replace("{most_used_feature}", most_used);
379        }
380
381        result = result.replace(
382            "{session_duration}",
383            &format!(
384                "{:.1}",
385                user_pattern
386                    .session_patterns
387                    .average_session_duration_minutes
388            ),
389        );
390
391        result = result.replace(
392            "{error_rate}",
393            &format!("{:.1}%", user_pattern.session_patterns.error_rate * 100.0),
394        );
395
396        result
397    }
398
399    /// Initialize default recommendation templates
400    async fn initialize_default_templates(&self) -> Result<(), AppError> {
401        let mut templates = self.recommendation_templates.write().await;
402
403        templates.push(RecommendationTemplate {
404            id: "voice_training".to_string(),
405            title_template: "Improve Voice Recognition Accuracy".to_string(),
406            description_template: "Your voice recognition accuracy could be improved. Try the voice training feature to personalize the system to your voice.".to_string(),
407            recommendation_type: RecommendationType::Feature,
408            category: "voice".to_string(),
409            tags: vec!["accuracy".to_string(), "training".to_string()],
410            conditions: vec![
411                RecommendationCondition::HasNotUsedFeature { feature: "voice_training".to_string() },
412                RecommendationCondition::ErrorRateAbove { threshold: 0.1 },
413            ],
414            priority: 8,
415            estimated_benefit: "Up to 30% improvement in voice recognition accuracy".to_string(),
416        });
417
418        templates.push(RecommendationTemplate {
419            id: "gesture_customization".to_string(),
420            title_template: "Customize Your Gestures".to_string(),
421            description_template: "You use gestures frequently! Create custom gestures for your most common actions to save time.".to_string(),
422            recommendation_type: RecommendationType::Feature,
423            category: "gestures".to_string(),
424            tags: vec!["customization".to_string(), "efficiency".to_string()],
425            conditions: vec![
426                RecommendationCondition::UsesFeatureFrequently { feature: "gestures".to_string(), min_usage: 50 },
427                RecommendationCondition::HasNotUsedFeature { feature: "custom_gestures".to_string() },
428            ],
429            priority: 7,
430            estimated_benefit: "Reduce gesture time by up to 40%".to_string(),
431        });
432
433        templates.push(RecommendationTemplate {
434            id: "session_optimization".to_string(),
435            title_template: "Optimize Your Workflow".to_string(),
436            description_template: "Your sessions are shorter than average ({session_duration} min). Try these workflow optimizations to be more productive.".to_string(),
437            recommendation_type: RecommendationType::Optimization,
438            category: "productivity".to_string(),
439            tags: vec!["workflow".to_string(), "efficiency".to_string()],
440            conditions: vec![
441                RecommendationCondition::SessionDurationBelow { threshold_minutes: 15.0 },
442            ],
443            priority: 6,
444            estimated_benefit: "Increase productivity by 25%".to_string(),
445        });
446
447        templates.push(RecommendationTemplate {
448            id: "ring_calibration".to_string(),
449            title_template: "Calibrate Your Ring".to_string(),
450            description_template: "Your gesture accuracy seems low. Try recalibrating your Haptic Harmony ring for better performance.".to_string(),
451            recommendation_type: RecommendationType::Setting,
452            category: "hardware".to_string(),
453            tags: vec!["calibration".to_string(), "accuracy".to_string()],
454            conditions: vec![
455                RecommendationCondition::GestureAccuracyBelow { threshold: 0.8 },
456            ],
457            priority: 9,
458            estimated_benefit: "Improve gesture accuracy by up to 50%".to_string(),
459        });
460
461        templates.push(RecommendationTemplate {
462            id: "tutorial_advanced".to_string(),
463            title_template: "Learn Advanced Features".to_string(),
464            description_template: "You're using {most_used_feature} frequently. Learn about advanced features that can enhance your experience.".to_string(),
465            recommendation_type: RecommendationType::Tutorial,
466            category: "learning".to_string(),
467            tags: vec!["advanced".to_string(), "features".to_string()],
468            conditions: vec![
469                RecommendationCondition::FeatureUsageAbove { feature: "basic_features".to_string(), threshold: 100 },
470            ],
471            priority: 5,
472            estimated_benefit: "Unlock 10+ advanced features".to_string(),
473        });
474
475        tracing::info!("Initialized {} recommendation templates", templates.len());
476        Ok(())
477    }
478
479    /// Record user feedback on recommendations
480    pub async fn record_feedback(
481        &self,
482        user_id: &str,
483        recommendation_id: &str,
484        feedback: RecommendationFeedback,
485    ) -> Result<(), AppError> {
486        let mut patterns = self.user_patterns.write().await;
487
488        if let Some(pattern) = patterns.get_mut(user_id) {
489            // Update preference scores based on feedback
490            let cache = self.recommendations_cache.read().await;
491            if let Some(recommendations) = cache.get(user_id)
492                && let Some(recommendation) =
493                    recommendations.iter().find(|r| r.id == recommendation_id)
494            {
495                let category = &recommendation.category;
496                let current_score = pattern.preference_scores.get(category).unwrap_or(&0.5);
497
498                let adjustment = match feedback {
499                    RecommendationFeedback::Helpful => 0.1,
500                    RecommendationFeedback::NotHelpful => -0.1,
501                    RecommendationFeedback::Implemented => 0.2,
502                    RecommendationFeedback::Dismissed => -0.05,
503                };
504
505                let new_score = (current_score + adjustment).clamp(0.0, 1.0);
506                pattern
507                    .preference_scores
508                    .insert(category.clone(), new_score);
509
510                tracing::debug!(
511                    "Updated preference score for category '{}' to {:.2} based on feedback",
512                    category,
513                    new_score
514                );
515            }
516        }
517
518        Ok(())
519    }
520
521    /// Get recommendation statistics
522    pub async fn get_stats(&self) -> serde_json::Value {
523        let patterns = self.user_patterns.read().await;
524        let cache = self.recommendations_cache.read().await;
525        let templates = self.recommendation_templates.read().await;
526
527        let total_users = patterns.len();
528        let total_cached_recommendations: usize = cache.values().map(|v| v.len()).sum();
529        let total_templates = templates.len();
530
531        serde_json::json!({
532            "total_users": total_users,
533            "total_cached_recommendations": total_cached_recommendations,
534            "total_templates": total_templates,
535            "average_recommendations_per_user": if total_users > 0 {
536                total_cached_recommendations as f64 / total_users as f64
537            } else {
538                0.0
539            }
540        })
541    }
542
543    /// Clear user data
544    pub async fn clear_user_data(&self, user_id: &str) -> Result<(), AppError> {
545        let mut patterns = self.user_patterns.write().await;
546        let mut cache = self.recommendations_cache.write().await;
547
548        patterns.remove(user_id);
549        cache.remove(user_id);
550
551        tracing::info!("Cleared recommendation data for user: {}", user_id);
552        Ok(())
553    }
554}
555
556impl Clone for PersonalizedRecommendationEngine {
557    fn clone(&self) -> Self {
558        Self {
559            user_patterns: self.user_patterns.clone(),
560            recommendations_cache: self.recommendations_cache.clone(),
561            global_patterns: self.global_patterns.clone(),
562            config: self.config.clone(),
563            recommendation_templates: self.recommendation_templates.clone(),
564        }
565    }
566}
567
568/// User feedback on recommendations
569#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
570pub enum RecommendationFeedback {
571    Helpful,
572    NotHelpful,
573    Implemented,
574    Dismissed,
575}
576
577/// Global recommendation engine instance
578static RECOMMENDATION_ENGINE: tokio::sync::OnceCell<PersonalizedRecommendationEngine> =
579    tokio::sync::OnceCell::const_new();
580
581/// Get the global recommendation engine
582pub async fn get_recommendation_engine() -> &'static PersonalizedRecommendationEngine {
583    RECOMMENDATION_ENGINE
584        .get_or_init(|| async {
585            PersonalizedRecommendationEngine::new(RecommendationConfig::default())
586        })
587        .await
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593
594    #[tokio::test]
595    async fn test_recommendation_generation() {
596        let engine = PersonalizedRecommendationEngine::new(RecommendationConfig::default());
597
598        // Wait a bit for background template initialization to complete
599        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
600
601        // Create user pattern
602        let usage_data = serde_json::json!({
603            "features": {
604                "voice_commands": 10,
605                "gestures": 50
606            },
607            "gestures": {
608                "tap": 30,
609                "swipe": 20
610            }
611        });
612
613        engine
614            .update_user_pattern("user1", usage_data)
615            .await
616            .unwrap();
617
618        let recommendations = engine.generate_recommendations("user1").await.unwrap();
619        assert!(!recommendations.is_empty());
620    }
621
622    #[tokio::test]
623    async fn test_feedback_recording() {
624        let engine = PersonalizedRecommendationEngine::new(RecommendationConfig::default());
625
626        // Setup user and generate recommendations
627        let usage_data = serde_json::json!({
628            "features": {"gestures": 100}
629        });
630
631        engine
632            .update_user_pattern("user1", usage_data)
633            .await
634            .unwrap();
635        let recommendations = engine.generate_recommendations("user1").await.unwrap();
636
637        if let Some(rec) = recommendations.first() {
638            engine
639                .record_feedback("user1", &rec.id, RecommendationFeedback::Helpful)
640                .await
641                .unwrap();
642        }
643
644        // Verify feedback was recorded (would check preference scores in real test)
645        let stats = engine.get_stats().await;
646        assert_eq!(stats["total_users"].as_u64().unwrap(), 1);
647    }
648}