1use super::types::{
7 AchievementAnalysis, AchievementCondition, AchievementDefinition, AggregatedMetric,
8 AnalyticsConfig, AnalyticsMetric, AnalyticsSummary, ComparativeAnalysis,
9 ComparativeAnalyticsResult, ComprehensiveAnalyticsReport, DetailedProgressReport, GoalAnalysis,
10 LongitudinalDataPoint, LongitudinalStudyData, MemoryBoundedMetrics, MetricType,
11 ProgressRecommendation, StatisticalSignificanceResult, TrendAnalysis, TrendAnalytics,
12};
13use crate::adaptive::RecommendationType;
14use crate::progress::analytics::TrendDirection;
15use crate::progress::dashboard::{
16 DashboardConfig, DashboardMetricsGenerator, RealTimeDashboardData,
17};
18use crate::progress::metrics::{
19 ConsistencyMetrics, MetricsCalculator, ProgressSystemStats, SessionAnalytics,
20};
21use crate::traits::{
22 Achievement, AchievementTier, FeedbackProvider, FeedbackResponse, FeedbackResult, FeedbackType,
23 FocusArea, Goal, GoalMetric, ProgressConfig, ProgressIndicators, ProgressReport,
24 ProgressSnapshot, ProgressTracker, ReportStatistics, SessionScores, SessionState,
25 SessionSummary, TimeRange, TrainingScores, TrainingStatistics, UserProgress,
26};
27use crate::FeedbackError;
28use async_trait::async_trait;
29use chrono::{DateTime, Utc};
30use serde::{Deserialize, Serialize};
31use statrs::statistics::Statistics;
32use std::collections::{HashMap, VecDeque};
33use std::sync::{Arc, RwLock};
34use std::time::Duration;
35use uuid::Uuid;
36use voirs_sdk::VoirsError;
37
38#[derive(Debug, Default)]
44struct ProgressMetrics {
45 total_users: usize,
47 total_sessions: usize,
49 total_snapshots: usize,
51}
52
53#[derive(Debug, Clone)]
59pub struct ComprehensiveAnalyticsFramework {
60 config: AnalyticsConfig,
62 metrics: MemoryBoundedMetrics,
64 aggregated_metrics: HashMap<String, AggregatedMetric>,
66 last_cleanup: DateTime<Utc>,
68}
69
70impl Default for ComprehensiveAnalyticsFramework {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl ComprehensiveAnalyticsFramework {
77 #[must_use]
79 pub fn new() -> Self {
80 let config = AnalyticsConfig::default();
81 Self {
82 metrics: MemoryBoundedMetrics::new(config.max_metrics_capacity),
83 aggregated_metrics: HashMap::new(),
84 last_cleanup: Utc::now(),
85 config,
86 }
87 }
88
89 #[must_use]
91 pub fn with_config(config: AnalyticsConfig) -> Self {
92 Self {
93 metrics: MemoryBoundedMetrics::new(config.max_metrics_capacity),
94 aggregated_metrics: HashMap::new(),
95 last_cleanup: Utc::now(),
96 config,
97 }
98 }
99
100 pub async fn generate_analytics_report(
102 &self,
103 progress: &UserProgress,
104 time_range: Option<TimeRange>,
105 ) -> Result<ComprehensiveAnalyticsReport, FeedbackError> {
106 let now = Utc::now();
107 let range = time_range.unwrap_or(TimeRange {
108 start: now - chrono::Duration::days(30),
109 end: now,
110 });
111
112 let mut metrics = vec![
114 AnalyticsMetric {
115 name: "overall_skill_level".to_string(),
116 value: f64::from(progress.overall_skill_level),
117 timestamp: now,
118 metric_type: MetricType::Gauge,
119 },
120 AnalyticsMetric {
121 name: "total_sessions".to_string(),
122 value: progress.training_stats.total_sessions as f64,
123 timestamp: now,
124 metric_type: MetricType::Counter,
125 },
126 AnalyticsMetric {
127 name: "success_rate".to_string(),
128 value: f64::from(progress.training_stats.success_rate),
129 timestamp: now,
130 metric_type: MetricType::Gauge,
131 },
132 AnalyticsMetric {
133 name: "average_improvement".to_string(),
134 value: f64::from(progress.training_stats.average_improvement),
135 timestamp: now,
136 metric_type: MetricType::Gauge,
137 },
138 AnalyticsMetric {
139 name: "current_streak".to_string(),
140 value: progress.training_stats.current_streak as f64,
141 timestamp: now,
142 metric_type: MetricType::Counter,
143 },
144 AnalyticsMetric {
145 name: "longest_streak".to_string(),
146 value: progress.training_stats.longest_streak as f64,
147 timestamp: now,
148 metric_type: MetricType::Counter,
149 },
150 ];
151
152 for (focus_area, &skill_level) in &progress.skill_breakdown {
154 metrics.push(AnalyticsMetric {
155 name: format!("skill_{focus_area:?}").to_lowercase(),
156 value: f64::from(skill_level),
157 timestamp: now,
158 metric_type: MetricType::Gauge,
159 });
160 }
161
162 let relevant_history: Vec<_> = progress
164 .progress_history
165 .iter()
166 .filter(|snapshot| snapshot.timestamp >= range.start && snapshot.timestamp <= range.end)
167 .collect();
168
169 let trend_analytics = self.calculate_trend_analytics(&relevant_history);
171
172 metrics.push(AnalyticsMetric {
174 name: "improvement_velocity".to_string(),
175 value: f64::from(trend_analytics.improvement_velocity),
176 timestamp: now,
177 metric_type: MetricType::Gauge,
178 });
179
180 metrics.push(AnalyticsMetric {
181 name: "performance_stability".to_string(),
182 value: f64::from(trend_analytics.performance_stability),
183 timestamp: now,
184 metric_type: MetricType::Gauge,
185 });
186
187 let summary = AnalyticsSummary {
188 total_metrics: metrics.len(),
189 average_value: if metrics.is_empty() {
190 0.0
191 } else {
192 metrics.iter().map(|m| m.value).sum::<f64>() / metrics.len() as f64
193 },
194 time_range: range,
195 };
196
197 Ok(ComprehensiveAnalyticsReport {
198 timestamp: now,
199 metrics,
200 summary,
201 })
202 }
203
204 fn calculate_trend_analytics(&self, history: &[&ProgressSnapshot]) -> TrendAnalytics {
206 if history.len() < 2 {
207 return TrendAnalytics {
208 improvement_velocity: 0.0,
209 performance_stability: 0.0,
210 trend_direction: TrendDirection::Stable,
211 slope: 0.0,
212 r_squared: 0.0,
213 };
214 }
215
216 let scores: Vec<f32> = history.iter().map(|h| h.overall_score).collect();
217
218 let mean = scores.iter().sum::<f32>() / scores.len() as f32;
220 let variance =
221 scores.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / scores.len() as f32;
222 let std_dev = variance.sqrt();
223
224 let (slope, r_squared) = if scores.len() >= 2 {
226 let x_values: Vec<f64> = (0..scores.len()).map(|i| i as f64).collect();
227 let y_values: Vec<f64> = scores.iter().map(|&s| f64::from(s)).collect();
228
229 let n = x_values.len() as f64;
231 let sum_x: f64 = x_values.iter().sum();
232 let sum_y: f64 = y_values.iter().sum();
233 let sum_xy: f64 = x_values.iter().zip(&y_values).map(|(x, y)| x * y).sum();
234 let sum_x2: f64 = x_values.iter().map(|x| x * x).sum();
235
236 let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
237
238 let y_mean = sum_y / n;
240 let ss_tot: f64 = y_values.iter().map(|y| (y - y_mean).powi(2)).sum();
241 let ss_res: f64 = y_values
242 .iter()
243 .zip(&x_values)
244 .map(|(y, x)| {
245 let predicted = slope * x + (sum_y - slope * sum_x) / n;
246 (y - predicted).powi(2)
247 })
248 .sum();
249
250 let r_squared = if ss_tot > 0.0 {
251 1.0 - ss_res / ss_tot
252 } else {
253 0.0
254 };
255
256 (slope as f32, r_squared.max(0.0) as f32)
257 } else {
258 (0.0, 0.0)
259 };
260
261 let trend_direction = if slope > 0.05 {
262 TrendDirection::Improving
263 } else if slope < -0.05 {
264 TrendDirection::Declining
265 } else {
266 TrendDirection::Stable
267 };
268
269 TrendAnalytics {
270 improvement_velocity: slope.max(0.0),
271 performance_stability: 1.0 / (1.0 + std_dev), trend_direction,
273 slope,
274 r_squared,
275 }
276 }
277
278 pub async fn test_statistical_significance(
280 &self,
281 _progress: &UserProgress,
282 _metric: AnalyticsMetric,
283 _time_period: Duration,
284 ) -> Result<StatisticalSignificanceResult, FeedbackError> {
285 Ok(StatisticalSignificanceResult {
287 p_value: 0.05,
288 is_significant: true,
289 confidence_level: 0.95,
290 effect_size: 0.3,
291 })
292 }
293
294 pub async fn generate_comparative_analysis(
296 &self,
297 user_progress_data: &[UserProgress],
298 _metric: AnalyticsMetric,
299 _time_range: Option<TimeRange>,
300 ) -> Result<ComparativeAnalyticsResult, FeedbackError> {
301 if user_progress_data.len() < 2 {
302 return Err(FeedbackError::ProgressTrackingError {
303 message: "Need at least 2 users for comparative analysis".to_string(),
304 source: None,
305 });
306 }
307
308 let baseline_value = f64::from(user_progress_data[0].overall_skill_level);
309 let comparison_value = f64::from(user_progress_data[1].overall_skill_level);
310 let percentage_change = ((comparison_value - baseline_value) / baseline_value) * 100.0;
311
312 Ok(ComparativeAnalyticsResult {
313 baseline_value,
314 comparison_value,
315 percentage_change,
316 statistical_significance: StatisticalSignificanceResult {
317 p_value: 0.05,
318 is_significant: percentage_change.abs() > 10.0,
319 confidence_level: 0.95,
320 effect_size: 0.3,
321 },
322 })
323 }
324
325 pub async fn collect_longitudinal_data(
327 &self,
328 _study_id: &str,
329 participant_data: &[UserProgress],
330 _tracking_metrics: &[AnalyticsMetric],
331 ) -> Result<LongitudinalStudyData, FeedbackError> {
332 let now = Utc::now();
333 let start = now - chrono::Duration::days(90);
334
335 let data_points: Vec<LongitudinalDataPoint> = participant_data
336 .iter()
337 .enumerate()
338 .map(|(i, progress)| LongitudinalDataPoint {
339 timestamp: start + chrono::Duration::days(i as i64),
340 value: f64::from(progress.overall_skill_level),
341 metadata: HashMap::new(),
342 })
343 .collect();
344
345 Ok(LongitudinalStudyData {
346 study_period: TimeRange { start, end: now },
347 data_points,
348 trend_analysis: TrendAnalysis {
349 trend_direction: TrendDirection::Improving,
350 slope: 0.1,
351 r_squared: 0.8,
352 },
353 })
354 }
355
356 fn update_aggregated_metric(&mut self, name: String, metric: &AnalyticsMetric) {
358 let aggregated = self
359 .aggregated_metrics
360 .entry(name.clone())
361 .or_insert_with(|| AggregatedMetric {
362 name: name.clone(),
363 count: 0,
364 sum: 0.0,
365 sum_of_squares: 0.0,
366 min: f64::INFINITY,
367 max: f64::NEG_INFINITY,
368 last_updated: metric.timestamp,
369 metric_type: metric.metric_type.clone(),
370 });
371
372 aggregated.count += 1;
373 aggregated.sum += metric.value;
374 aggregated.sum_of_squares += metric.value * metric.value;
375 aggregated.min = aggregated.min.min(metric.value);
376 aggregated.max = aggregated.max.max(metric.value);
377 aggregated.last_updated = metric.timestamp;
378 }
379}
380
381#[derive(Clone)]
387pub struct ProgressAnalyzer {
388 user_progress: Arc<RwLock<HashMap<String, UserProgress>>>,
390 achievements: Arc<RwLock<Vec<AchievementDefinition>>>,
392 config: ProgressConfig,
394 metrics: Arc<RwLock<ProgressMetrics>>,
396 analytics: Arc<RwLock<ComprehensiveAnalyticsFramework>>,
398}
399
400impl ProgressAnalyzer {
401 pub async fn new() -> Result<Self, FeedbackError> {
403 Self::with_config(ProgressConfig::default()).await
404 }
405
406 pub async fn with_config(config: ProgressConfig) -> Result<Self, FeedbackError> {
408 let achievements = Self::create_default_achievements();
409
410 Ok(Self {
411 user_progress: Arc::new(RwLock::new(HashMap::new())),
412 achievements: Arc::new(RwLock::new(achievements)),
413 config,
414 metrics: Arc::new(RwLock::new(ProgressMetrics::default())),
415 analytics: Arc::new(RwLock::new(ComprehensiveAnalyticsFramework::new())),
416 })
417 }
418
419 pub async fn get_user_progress_impl(
421 &self,
422 user_id: &str,
423 ) -> Result<UserProgress, FeedbackError> {
424 {
425 let progress_map = self.user_progress.read().unwrap();
426 if let Some(progress) = progress_map.get(user_id) {
427 return Ok(progress.clone());
428 }
429 } self.create_user_progress(user_id).await
431 }
432
433 async fn create_user_progress(&self, user_id: &str) -> Result<UserProgress, FeedbackError> {
435 let progress = UserProgress {
436 user_id: user_id.to_string(),
437 overall_skill_level: 0.5,
438 skill_breakdown: Self::initialize_skill_breakdown(),
439 progress_history: Vec::new(),
440 achievements: Vec::new(),
441 training_stats: TrainingStatistics {
442 total_sessions: 0,
443 successful_sessions: 0,
444 total_training_time: Duration::from_secs(0),
445 exercises_completed: 0,
446 success_rate: 0.0,
447 average_improvement: 0.0,
448 current_streak: 0,
449 longest_streak: 0,
450 },
451 goals: Vec::new(),
452 last_updated: Utc::now(),
453 average_scores: SessionScores::default(),
454 skill_levels: HashMap::new(),
455 recent_sessions: Vec::new(),
456 personal_bests: HashMap::new(),
457 session_count: 0,
458 total_practice_time: Duration::from_secs(0),
459 };
460
461 {
462 let mut progress_map = self.user_progress.write().unwrap();
463 progress_map.insert(user_id.to_string(), progress.clone());
464 }
465
466 {
467 let mut metrics = self.metrics.write().unwrap();
468 metrics.total_users += 1;
469 }
470
471 Ok(progress)
472 }
473
474 pub async fn record_training_session(
476 &self,
477 user_id: &str,
478 scores: &TrainingScores,
479 ) -> Result<(), FeedbackError> {
480 let feedback = FeedbackResponse {
482 feedback_items: Vec::new(),
483 overall_score: (scores.pronunciation + scores.quality + scores.consistency) / 3.0,
484 immediate_actions: Vec::new(),
485 long_term_goals: Vec::new(),
486 progress_indicators: ProgressIndicators {
487 improving_areas: Vec::new(),
488 attention_areas: Vec::new(),
489 stable_areas: Vec::new(),
490 overall_trend: scores.improvement,
491 completion_percentage: scores.pronunciation * 100.0,
492 },
493 timestamp: Utc::now(),
494 processing_time: Duration::from_millis(100),
495 feedback_type: FeedbackType::Quality,
496 };
497
498 let session = SessionState {
499 session_id: Uuid::new_v4(),
500 user_id: user_id.to_string(),
501 start_time: Utc::now(),
502 last_activity: Utc::now(),
503 current_task: None,
504 stats: Default::default(),
505 preferences: Default::default(),
506 adaptive_state: Default::default(),
507 current_exercise: None,
508 session_stats: Default::default(),
509 };
510
511 self.record_session_progress(user_id, &session, scores, &feedback)
512 .await
513 }
514
515 pub async fn record_session_progress(
517 &self,
518 user_id: &str,
519 session: &SessionState,
520 scores: &TrainingScores,
521 feedback: &FeedbackResponse,
522 ) -> Result<(), FeedbackError> {
523 let _ = self.get_user_progress_impl(user_id).await?;
525
526 let progress_for_achievements = {
528 let mut progress_map = self.user_progress.write().unwrap();
529 let progress = progress_map.get_mut(user_id).ok_or_else(|| {
530 FeedbackError::ProgressTrackingError {
531 message: format!("User progress not found: {user_id}"),
532 source: None,
533 }
534 })?;
535
536 let session_score = (scores.pronunciation + scores.quality + scores.consistency) / 3.0;
538 progress.overall_skill_level =
539 self.update_skill_level(progress.overall_skill_level, session_score);
540
541 self.update_skill_breakdown(progress, scores, feedback)?;
543
544 let snapshot = ProgressSnapshot {
546 timestamp: Utc::now(),
547 overall_score: session_score,
548 area_scores: self.extract_area_scores(scores, feedback),
549 session_count: (progress.training_stats.total_sessions + 1),
550 events: self.extract_session_events(feedback),
551 };
552
553 progress.progress_history.push(snapshot);
555
556 if progress.progress_history.len() > 1000 {
558 progress.progress_history.drain(0..100); }
560
561 self.update_training_stats(&mut progress.training_stats, session, scores);
563
564 self.update_streak_info(&mut progress.training_stats, scores);
566
567 let session_summary = SessionSummary {
569 session_id: session.session_id.to_string(),
570 timestamp: session.start_time,
571 duration: Duration::from_secs(300), score: (scores.pronunciation + scores.quality + scores.consistency) / 3.0,
573 exercises_completed: 1,
574 };
575
576 progress.recent_sessions.push(session_summary);
577 progress.session_count += 1;
578 progress.total_practice_time += Duration::from_secs(300);
579
580 if progress.recent_sessions.len() > 50 {
582 progress
583 .recent_sessions
584 .drain(0..progress.recent_sessions.len() - 50);
585 }
586
587 progress.clone()
589 };
590
591 let new_achievements = self
593 .check_achievements_for_user(&progress_for_achievements)
594 .await?;
595
596 {
598 let mut progress_map = self.user_progress.write().unwrap();
599 if let Some(progress) = progress_map.get_mut(user_id) {
600 progress.achievements.extend(new_achievements);
601 progress.last_updated = Utc::now();
602 }
603 }
604
605 {
607 let mut metrics = self.metrics.write().unwrap();
608 metrics.total_sessions += 1;
609 metrics.total_snapshots += 1;
610 }
611
612 Ok(())
613 }
614
615 pub async fn generate_detailed_report(
617 &self,
618 user_id: &str,
619 time_range: Option<TimeRange>,
620 ) -> Result<DetailedProgressReport, FeedbackError> {
621 let progress = self.get_user_progress_impl(user_id).await?;
622
623 let (start_date, end_date) = if let Some(range) = time_range {
624 (range.start, range.end)
625 } else {
626 let end = Utc::now();
628 let start = end - chrono::Duration::days(30);
629 (start, end)
630 };
631
632 let relevant_history: Vec<_> = progress
634 .progress_history
635 .iter()
636 .filter(|snapshot| snapshot.timestamp >= start_date && snapshot.timestamp <= end_date)
637 .collect();
638
639 if relevant_history.is_empty() {
640 return Err(FeedbackError::ProgressTrackingError {
641 message: "No progress data found for the specified time range".to_string(),
642 source: None,
643 });
644 }
645
646 let report = DetailedProgressReport {
647 user_id: user_id.to_string(),
648 period: TimeRange {
649 start: start_date,
650 end: end_date,
651 },
652 overall_improvement: MetricsCalculator::calculate_overall_improvement(
653 &relevant_history,
654 ),
655 area_improvements: MetricsCalculator::calculate_area_improvements(&relevant_history),
656 skill_trends: MetricsCalculator::calculate_skill_trends(&relevant_history),
657 session_analytics: MetricsCalculator::calculate_session_analytics(&relevant_history),
658 consistency_metrics: MetricsCalculator::calculate_consistency_metrics(
659 &relevant_history,
660 ),
661 achievement_progress: self.analyze_achievement_progress(&progress),
662 goal_analysis: self.analyze_goal_progress(&progress),
663 recommendations: self.generate_progress_recommendations(&progress, &relevant_history),
664 comparative_analysis: self
665 .generate_comparative_analysis(user_id, &relevant_history)
666 .await?,
667 };
668
669 Ok(report)
670 }
671
672 pub async fn analyze_learning_patterns(
674 &self,
675 user_id: &str,
676 ) -> Result<Vec<String>, FeedbackError> {
677 let progress = self.get_user_progress_impl(user_id).await?;
678
679 if progress.progress_history.len() < 5 {
680 return Ok(vec!["Insufficient data for pattern analysis".to_string()]);
681 }
682
683 let mut patterns = Vec::new();
684
685 let recent_scores: Vec<f32> = progress
687 .progress_history
688 .iter()
689 .rev()
690 .take(10)
691 .map(|s| s.overall_score)
692 .collect();
693
694 if let (Some(&first), Some(&last)) = (recent_scores.last(), recent_scores.first()) {
695 if last > first + 0.1 {
696 patterns.push(
697 "You're improving consistently and making great progress in recent sessions"
698 .to_string(),
699 );
700 } else if last < first - 0.1 {
701 patterns.push("Consider reviewing your practice approach".to_string());
702 } else {
703 patterns.push("Your performance is stable".to_string());
704 }
705 }
706
707 if progress.training_stats.current_streak >= 3 {
709 patterns.push("Great job maintaining a practice streak!".to_string());
710 }
711
712 if let Some((best_area, &best_score)) = progress
714 .skill_breakdown
715 .iter()
716 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
717 {
718 patterns.push(format!(
719 "Your strongest area is {best_area:?} ({:.1}%)",
720 best_score * 100.0
721 ));
722 }
723
724 Ok(patterns)
725 }
726
727 pub async fn set_goal(&self, user_id: &str, goal: Goal) -> Result<(), FeedbackError> {
729 let _ = self.get_user_progress_impl(user_id).await?;
731
732 let mut progress_map = self.user_progress.write().unwrap();
733 let progress =
734 progress_map
735 .get_mut(user_id)
736 .ok_or_else(|| FeedbackError::ProgressTrackingError {
737 message: format!("User progress not found: {user_id}"),
738 source: None,
739 })?;
740
741 self.validate_goal(&goal)?;
743
744 progress.goals.push(goal);
745 progress.last_updated = Utc::now();
746
747 Ok(())
748 }
749
750 pub async fn get_user_goals(&self, user_id: &str) -> Result<Vec<Goal>, FeedbackError> {
752 let progress = self.get_user_progress_impl(user_id).await?;
753 Ok(progress.goals)
754 }
755
756 pub async fn check_achievements(
758 &self,
759 user_id: &str,
760 ) -> Result<Vec<Achievement>, FeedbackError> {
761 let progress = self.get_user_progress_impl(user_id).await?;
762 Ok(progress.achievements)
763 }
764
765 pub async fn get_statistics(&self) -> Result<ProgressSystemStats, FeedbackError> {
767 let progress_map = self.user_progress.read().unwrap();
768 let metrics = self.metrics.read().unwrap();
769
770 let active_users = progress_map.len();
771 let total_achievements = progress_map.values().map(|p| p.achievements.len()).sum();
772
773 let average_skill_level = if progress_map.is_empty() {
774 0.0
775 } else {
776 let scores: Vec<f32> = progress_map
777 .values()
778 .map(|p| p.overall_skill_level)
779 .collect();
780 scores.iter().sum::<f32>() / scores.len() as f32
781 };
782
783 Ok(ProgressSystemStats {
784 total_users: metrics.total_users,
785 active_users,
786 total_sessions: metrics.total_sessions,
787 total_achievements_unlocked: total_achievements,
788 average_skill_level,
789 total_snapshots: metrics.total_snapshots,
790 })
791 }
792
793 fn initialize_skill_breakdown() -> HashMap<FocusArea, f32> {
799 [
800 (FocusArea::Pronunciation, 0.5),
801 (FocusArea::Fluency, 0.5),
802 (FocusArea::Naturalness, 0.5),
803 (FocusArea::Quality, 0.5),
804 (FocusArea::Rhythm, 0.5),
805 (FocusArea::Stress, 0.5),
806 ]
807 .iter()
808 .cloned()
809 .collect()
810 }
811
812 fn update_skill_level(&self, current_level: f32, session_score: f32) -> f32 {
814 let learning_rate = 0.1;
816 current_level * (1.0 - learning_rate) + session_score * learning_rate
817 }
818
819 fn update_skill_breakdown(
821 &self,
822 progress: &mut UserProgress,
823 scores: &TrainingScores,
824 _feedback: &FeedbackResponse,
825 ) -> Result<(), FeedbackError> {
826 if let Some(pronunciation_level) =
828 progress.skill_breakdown.get_mut(&FocusArea::Pronunciation)
829 {
830 *pronunciation_level =
831 self.update_skill_level(*pronunciation_level, scores.pronunciation);
832 }
833
834 if let Some(naturalness_level) = progress.skill_breakdown.get_mut(&FocusArea::Naturalness) {
836 *naturalness_level = self.update_skill_level(*naturalness_level, scores.quality);
837 }
838
839 if let Some(rhythm_level) = progress.skill_breakdown.get_mut(&FocusArea::Rhythm) {
841 *rhythm_level = self.update_skill_level(*rhythm_level, scores.consistency);
842 }
843
844 Ok(())
845 }
846
847 fn extract_area_scores(
849 &self,
850 scores: &TrainingScores,
851 _feedback: &FeedbackResponse,
852 ) -> HashMap<FocusArea, f32> {
853 [
854 (FocusArea::Pronunciation, scores.pronunciation),
855 (FocusArea::Naturalness, scores.quality),
856 (FocusArea::Rhythm, scores.consistency),
857 ]
858 .iter()
859 .cloned()
860 .collect()
861 }
862
863 fn extract_session_events(&self, feedback: &FeedbackResponse) -> Vec<String> {
865 feedback
866 .feedback_items
867 .iter()
868 .map(|item| item.message.clone())
869 .collect()
870 }
871
872 fn update_training_stats(
874 &self,
875 stats: &mut TrainingStatistics,
876 session: &SessionState,
877 scores: &TrainingScores,
878 ) {
879 stats.total_sessions += 1;
880
881 let session_duration = Utc::now().signed_duration_since(session.start_time);
883 stats.total_training_time += session_duration
884 .to_std()
885 .unwrap_or(Duration::from_secs(1800)); stats.exercises_completed += 5; let session_success = usize::from(scores.pronunciation >= 0.7 && scores.quality >= 0.7);
892 stats.successful_sessions += session_success;
893 stats.success_rate = stats.successful_sessions as f32 / stats.total_sessions as f32;
894
895 if stats.total_sessions > 1 {
897 stats.average_improvement = (stats.average_improvement
898 * (stats.total_sessions - 1) as f32
899 + scores.improvement)
900 / stats.total_sessions as f32;
901 } else {
902 stats.average_improvement = scores.improvement;
903 }
904 }
905
906 fn update_streak_info(&self, stats: &mut TrainingStatistics, scores: &TrainingScores) {
908 let avg_score = (scores.pronunciation + scores.quality + scores.consistency) / 3.0;
910 if avg_score >= 0.7 {
911 stats.current_streak += 1;
912 stats.longest_streak = stats.longest_streak.max(stats.current_streak);
913 } else {
914 stats.current_streak = 0;
915 }
916 }
917
918 async fn check_achievements_for_user(
920 &self,
921 progress: &UserProgress,
922 ) -> Result<Vec<Achievement>, FeedbackError> {
923 let achievements = self.achievements.read().unwrap();
924 let mut new_achievements = Vec::new();
925
926 for achievement_def in achievements.iter() {
927 if progress
929 .achievements
930 .iter()
931 .any(|a| a.achievement_id == achievement_def.id)
932 {
933 continue;
934 }
935
936 if self.is_achievement_condition_met(progress, &achievement_def.condition) {
938 new_achievements.push(Achievement {
939 achievement_id: achievement_def.id.clone(),
940 name: achievement_def.name.clone(),
941 description: achievement_def.description.clone(),
942 tier: achievement_def.tier.clone(),
943 points: achievement_def.points,
944 unlocked_at: Utc::now(),
945 });
946 }
947 }
948
949 Ok(new_achievements)
950 }
951
952 fn is_achievement_condition_met(
954 &self,
955 progress: &UserProgress,
956 condition: &AchievementCondition,
957 ) -> bool {
958 match condition {
959 AchievementCondition::SessionCount(required) => {
960 progress.training_stats.total_sessions >= *required
961 }
962 AchievementCondition::SkillLevel(required) => progress.overall_skill_level >= *required,
963 AchievementCondition::Streak(required) => {
964 progress.training_stats.longest_streak >= *required
965 }
966 AchievementCondition::AreaMastery(area, required) => {
967 progress.skill_breakdown.get(area).unwrap_or(&0.0) >= required
968 }
969 AchievementCondition::TrainingTime(required) => {
970 progress.training_stats.total_training_time >= *required
971 }
972 }
973 }
974
975 fn validate_goal(&self, goal: &Goal) -> Result<(), FeedbackError> {
977 if goal.target_value < 0.0 || goal.target_value > 1.0 {
978 return Err(FeedbackError::ProgressTrackingError {
979 message: "Goal target value must be between 0.0 and 1.0".to_string(),
980 source: None,
981 });
982 }
983 Ok(())
984 }
985
986 fn analyze_achievement_progress(&self, progress: &UserProgress) -> Vec<AchievementAnalysis> {
988 let achievements = self.achievements.read().unwrap();
989 let mut analyses = Vec::new();
990
991 for achievement_def in achievements.iter() {
992 let current_progress = self.calculate_achievement_progress(progress, achievement_def);
993 let is_unlocked = progress
994 .achievements
995 .iter()
996 .any(|a| a.achievement_id == achievement_def.id);
997
998 analyses.push(AchievementAnalysis {
999 achievement_id: achievement_def.id.clone(),
1000 name: achievement_def.name.clone(),
1001 current_progress,
1002 is_unlocked,
1003 estimated_time_to_unlock: None, });
1005 }
1006
1007 analyses
1008 }
1009
1010 fn calculate_achievement_progress(
1012 &self,
1013 progress: &UserProgress,
1014 achievement_def: &AchievementDefinition,
1015 ) -> f32 {
1016 match &achievement_def.condition {
1017 AchievementCondition::SessionCount(required) => {
1018 (progress.training_stats.total_sessions as f32 / *required as f32).min(1.0)
1019 }
1020 AchievementCondition::SkillLevel(required) => {
1021 (progress.overall_skill_level / required).min(1.0)
1022 }
1023 AchievementCondition::Streak(required) => {
1024 (progress.training_stats.longest_streak as f32 / *required as f32).min(1.0)
1025 }
1026 AchievementCondition::AreaMastery(area, required) => {
1027 let current = progress.skill_breakdown.get(area).unwrap_or(&0.0);
1028 (current / required).min(1.0)
1029 }
1030 AchievementCondition::TrainingTime(required) => {
1031 let current_secs = progress.training_stats.total_training_time.as_secs() as f32;
1032 let required_secs = required.as_secs() as f32;
1033 (current_secs / required_secs).min(1.0)
1034 }
1035 }
1036 }
1037
1038 fn analyze_goal_progress(&self, progress: &UserProgress) -> Vec<GoalAnalysis> {
1040 progress
1041 .goals
1042 .iter()
1043 .map(|goal| {
1044 let current_value = match &goal.target_metric {
1045 GoalMetric::OverallSkill => progress.overall_skill_level,
1046 GoalMetric::FocusAreaSkill(area) => {
1047 *progress.skill_breakdown.get(area).unwrap_or(&0.0)
1048 }
1049 GoalMetric::ExerciseCount => progress.training_stats.exercises_completed as f32,
1050 GoalMetric::SessionCount => progress.training_stats.total_sessions as f32,
1051 GoalMetric::TrainingTime => {
1052 progress.training_stats.total_training_time.as_secs() as f32
1053 }
1054 GoalMetric::Streak => progress.training_stats.current_streak as f32,
1055 };
1056
1057 let progress_percentage = (current_value / goal.target_value * 100.0).min(100.0);
1058
1059 GoalAnalysis {
1060 goal: goal.clone(),
1061 current_value,
1062 progress_percentage,
1063 on_track: progress_percentage >= 50.0, estimated_completion: None, }
1066 })
1067 .collect()
1068 }
1069
1070 fn generate_progress_recommendations(
1072 &self,
1073 progress: &UserProgress,
1074 _history: &[&ProgressSnapshot],
1075 ) -> Vec<ProgressRecommendation> {
1076 let mut recommendations = Vec::new();
1077
1078 for (area, &score) in &progress.skill_breakdown {
1080 if score < 0.6 {
1081 recommendations.push(ProgressRecommendation {
1082 recommendation_type: RecommendationType::Practice,
1083 title: format!("Focus on {area:?}"),
1084 description: format!(
1085 "Your {area:?} skills could use some attention. Current level: {:.0}%",
1086 score * 100.0
1087 ),
1088 priority: 1.0 - score, estimated_impact: 0.8,
1090 suggested_actions: vec![
1091 format!("Practice {area:?} exercises for 15 minutes daily"),
1092 "Review related learning materials".to_string(),
1093 ],
1094 });
1095 }
1096 }
1097
1098 if progress.training_stats.current_streak == 0 {
1100 recommendations.push(ProgressRecommendation {
1101 recommendation_type: RecommendationType::Consistency,
1102 title: "Build a Practice Streak".to_string(),
1103 description: "Start building a daily practice habit".to_string(),
1104 priority: 0.7,
1105 estimated_impact: 0.9,
1106 suggested_actions: vec![
1107 "Set a daily practice reminder".to_string(),
1108 "Start with short 10-minute sessions".to_string(),
1109 ],
1110 });
1111 }
1112
1113 recommendations
1114 }
1115
1116 async fn generate_comparative_analysis(
1118 &self,
1119 user_id: &str,
1120 _history: &[&ProgressSnapshot],
1121 ) -> Result<ComparativeAnalysis, FeedbackError> {
1122 let progress = self.get_user_progress_impl(user_id).await?;
1123 let stats = self.get_statistics().await?;
1124
1125 Ok(ComparativeAnalysis {
1126 user_percentile: 50.0, average_user_score: stats.average_skill_level,
1128 user_score: progress.overall_skill_level,
1129 improvement_rate_vs_average: 0.0, strengths_vs_peers: vec!["Consistent practice".to_string()],
1131 areas_for_improvement: vec!["Pronunciation".to_string()],
1132 })
1133 }
1134
1135 fn create_default_achievements() -> Vec<AchievementDefinition> {
1137 vec![
1138 AchievementDefinition {
1139 id: "first_session".to_string(),
1140 name: "First Steps".to_string(),
1141 description: "Complete your first training session".to_string(),
1142 condition: AchievementCondition::SessionCount(1),
1143 tier: AchievementTier::Bronze,
1144 points: 10,
1145 },
1146 AchievementDefinition {
1147 id: "ten_sessions".to_string(),
1148 name: "Getting Started".to_string(),
1149 description: "Complete 10 training sessions".to_string(),
1150 condition: AchievementCondition::SessionCount(10),
1151 tier: AchievementTier::Bronze,
1152 points: 50,
1153 },
1154 AchievementDefinition {
1155 id: "three_day_streak".to_string(),
1156 name: "Streak Starter".to_string(),
1157 description: "Maintain a 3-day practice streak".to_string(),
1158 condition: AchievementCondition::Streak(3),
1159 tier: AchievementTier::Bronze,
1160 points: 30,
1161 },
1162 AchievementDefinition {
1163 id: "pronunciation_master".to_string(),
1164 name: "Pronunciation Master".to_string(),
1165 description: "Achieve 90% pronunciation accuracy".to_string(),
1166 condition: AchievementCondition::AreaMastery(FocusArea::Pronunciation, 0.9),
1167 tier: AchievementTier::Gold,
1168 points: 100,
1169 },
1170 ]
1171 }
1172}
1173
1174#[async_trait]
1179impl ProgressTracker for ProgressAnalyzer {
1180 async fn record_progress(
1181 &mut self,
1182 user_id: &str,
1183 session: &SessionState,
1184 scores: &TrainingScores,
1185 ) -> FeedbackResult<()> {
1186 let feedback = FeedbackResponse {
1187 feedback_items: Vec::new(),
1188 overall_score: (scores.pronunciation + scores.quality + scores.consistency) / 3.0,
1189 immediate_actions: Vec::new(),
1190 long_term_goals: Vec::new(),
1191 progress_indicators: ProgressIndicators {
1192 improving_areas: Vec::new(),
1193 attention_areas: Vec::new(),
1194 stable_areas: Vec::new(),
1195 overall_trend: scores.improvement,
1196 completion_percentage: scores.pronunciation * 100.0,
1197 },
1198 timestamp: Utc::now(),
1199 processing_time: Duration::from_millis(100),
1200 feedback_type: FeedbackType::Quality,
1201 };
1202
1203 self.record_session_progress(user_id, session, scores, &feedback)
1204 .await
1205 .map_err(std::convert::Into::into)
1206 }
1207
1208 async fn get_user_progress(&self, user_id: &str) -> FeedbackResult<UserProgress> {
1209 self.get_user_progress_impl(user_id)
1210 .await
1211 .map_err(std::convert::Into::into)
1212 }
1213
1214 async fn generate_progress_report(
1215 &self,
1216 user_id: &str,
1217 time_range: Option<TimeRange>,
1218 ) -> FeedbackResult<ProgressReport> {
1219 let progress = self.get_user_progress_impl(user_id).await?;
1220
1221 let period = time_range.unwrap_or_else(|| {
1222 let now = Utc::now();
1223 TimeRange {
1224 start: now - chrono::Duration::days(30),
1225 end: now,
1226 }
1227 });
1228
1229 Ok(ProgressReport {
1230 user_id: progress.user_id,
1231 period,
1232 overall_improvement: 0.1, area_improvements: progress.skill_breakdown,
1234 achievements: progress.achievements,
1235 goal_progress: Vec::new(), recommendations: vec!["Keep practicing daily".to_string()],
1237 statistics: ReportStatistics {
1238 sessions_count: progress.training_stats.total_sessions,
1239 total_practice_time: progress.training_stats.total_training_time,
1240 average_session_length: Duration::from_secs(1800), exercises_completed: progress.training_stats.exercises_completed,
1242 success_rate: progress.training_stats.success_rate,
1243 },
1244 })
1245 }
1246
1247 async fn check_achievements(&self, user_id: &str) -> FeedbackResult<Vec<Achievement>> {
1248 let progress = self.get_user_progress(user_id).await?;
1249 Ok(progress.achievements)
1250 }
1251
1252 async fn set_goals(&mut self, user_id: &str, goals: Vec<Goal>) -> FeedbackResult<()> {
1253 let mut progress_map = self.user_progress.write().unwrap();
1254 let progress = progress_map.get_mut(user_id).ok_or_else(|| {
1255 VoirsError::from(FeedbackError::ProgressTrackingError {
1256 message: format!("User progress not found: {user_id}"),
1257 source: None,
1258 })
1259 })?;
1260
1261 for goal in &goals {
1263 self.validate_goal(goal).map_err(VoirsError::from)?;
1264 }
1265
1266 progress.goals = goals;
1267 progress.last_updated = Utc::now();
1268
1269 Ok(())
1270 }
1271}