using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Services; namespace AxCopilot.ViewModels; public class StatisticsViewModel : INotifyPropertyChanged { private class FavFileEntry { [JsonPropertyName("name")] public string Name { get; set; } = ""; [JsonPropertyName("path")] public string Path { get; set; } = ""; } private const double MaxBarHeight = 66.0; private const double MaxBarWidth = 200.0; private bool _hasFavorites; private string _todayDate = ""; private string _todaySummary = ""; private string _peakDate = ""; private string _peakDaySummary = ""; private string _weekSummary = ""; private string _totalOpensSummary = ""; private string _agentSummary = ""; private double _promptBarWidth; private double _completionBarWidth; private string _promptTokenLabel = ""; private string _completionTokenLabel = ""; private string _tokenRatioSummary = ""; public ObservableCollection LauncherOpenBars { get; } = new ObservableCollection(); public ObservableCollection ActiveTimeBars { get; } = new ObservableCollection(); public ObservableCollection TopCommands { get; } = new ObservableCollection(); public ObservableCollection TopFavorites { get; } = new ObservableCollection(); public ObservableCollection ChatCountBars { get; } = new ObservableCollection(); public ObservableCollection TokenUsageBars { get; } = new ObservableCollection(); public ObservableCollection WeekdayAvgBars { get; } = new ObservableCollection(); public ObservableCollection TabChatRatios { get; } = new ObservableCollection(); public bool HasFavorites { get { return _hasFavorites; } private set { _hasFavorites = value; OnPropertyChanged("HasFavorites"); } } public string TodayDate { get { return _todayDate; } set { _todayDate = value; OnPropertyChanged("TodayDate"); } } public string TodaySummary { get { return _todaySummary; } set { _todaySummary = value; OnPropertyChanged("TodaySummary"); } } public string PeakDate { get { return _peakDate; } set { _peakDate = value; OnPropertyChanged("PeakDate"); } } public string PeakDaySummary { get { return _peakDaySummary; } set { _peakDaySummary = value; OnPropertyChanged("PeakDaySummary"); } } public string WeekSummary { get { return _weekSummary; } set { _weekSummary = value; OnPropertyChanged("WeekSummary"); } } public string TotalOpensSummary { get { return _totalOpensSummary; } set { _totalOpensSummary = value; OnPropertyChanged("TotalOpensSummary"); } } public string AgentSummary { get { return _agentSummary; } set { _agentSummary = value; OnPropertyChanged("AgentSummary"); } } public double PromptBarWidth { get { return _promptBarWidth; } set { _promptBarWidth = value; OnPropertyChanged("PromptBarWidth"); } } public double CompletionBarWidth { get { return _completionBarWidth; } set { _completionBarWidth = value; OnPropertyChanged("CompletionBarWidth"); } } public string PromptTokenLabel { get { return _promptTokenLabel; } set { _promptTokenLabel = value; OnPropertyChanged("PromptTokenLabel"); } } public string CompletionTokenLabel { get { return _completionTokenLabel; } set { _completionTokenLabel = value; OnPropertyChanged("CompletionTokenLabel"); } } public string TokenRatioSummary { get { return _tokenRatioSummary; } set { _tokenRatioSummary = value; OnPropertyChanged("TokenRatioSummary"); } } public event PropertyChangedEventHandler? PropertyChanged; public StatisticsViewModel() { Refresh(); } public void Refresh() { List stats = UsageStatisticsService.GetStats(); string today = DateTime.Today.ToString("yyyy-MM-dd"); BuildLauncherOpenChart(stats, today); BuildActiveTimeChart(stats, today); BuildChatCountChart(stats, today); BuildTokenUsageChart(stats, today); BuildTopCommands(stats); BuildWeekdayAvgChart(stats); BuildTabChatRatios(stats); BuildTokenRatio(stats); BuildSummaryCards(stats, today); BuildTopFavorites(); } private void BuildLauncherOpenChart(List stats, string today) { List list = stats.TakeLast(14).ToList(); int num = list.Max((DailyUsageStats s) => s.LauncherOpens); if (num == 0) { num = 1; } LauncherOpenBars.Clear(); foreach (DailyUsageStats item in list) { DateTime result; DateTime dateTime = (DateTime.TryParse(item.Date, out result) ? result : DateTime.MinValue); LauncherOpenBars.Add(new DayBarItem { DateLabel = ((dateTime != DateTime.MinValue) ? $"{dateTime.Month}/{dateTime.Day}" : ""), DayLabel = ((dateTime != DateTime.MinValue) ? GetKoreanDayOfWeek(dateTime.DayOfWeek) : ""), Value = item.LauncherOpens, BarHeight = 66.0 * (double)item.LauncherOpens / (double)num, ValueLabel = ((item.LauncherOpens > 0) ? item.LauncherOpens.ToString() : ""), ToolTipText = ((dateTime != DateTime.MinValue) ? $"{dateTime:yyyy-MM-dd} ({GetKoreanDayOfWeek(dateTime.DayOfWeek)})\n호출 {item.LauncherOpens}회" : ""), IsToday = (item.Date == today) }); } } private void BuildActiveTimeChart(List stats, string today) { List list = stats.TakeLast(14).ToList(); int num = list.Max((DailyUsageStats s) => s.ActiveSeconds); if (num == 0) { num = 1; } ActiveTimeBars.Clear(); foreach (DailyUsageStats item in list) { DateTime result; DateTime dateTime = (DateTime.TryParse(item.Date, out result) ? result : DateTime.MinValue); ActiveTimeBars.Add(new DayBarItem { DateLabel = ((dateTime != DateTime.MinValue) ? $"{dateTime.Month}/{dateTime.Day}" : ""), DayLabel = ((dateTime != DateTime.MinValue) ? GetKoreanDayOfWeek(dateTime.DayOfWeek) : ""), Value = item.ActiveSeconds, BarHeight = 66.0 * (double)item.ActiveSeconds / (double)num, ValueLabel = FormatActiveTime(item.ActiveSeconds), ToolTipText = ((dateTime != DateTime.MinValue) ? $"{dateTime:yyyy-MM-dd} ({GetKoreanDayOfWeek(dateTime.DayOfWeek)})\n활성 {FormatActiveTime(item.ActiveSeconds)}" : ""), IsToday = (item.Date == today) }); } } private void BuildChatCountChart(List stats, string today) { List list = stats.TakeLast(14).ToList(); int num = list.Max(delegate(DailyUsageStats s) { int num3 = 0; foreach (KeyValuePair chatCount in s.ChatCounts) { num3 += chatCount.Value; } return num3; }); if (num == 0) { num = 1; } ChatCountBars.Clear(); foreach (DailyUsageStats item in list) { DateTime result; DateTime value = (DateTime.TryParse(item.Date, out result) ? result : DateTime.MinValue); int valueOrDefault = item.ChatCounts.GetValueOrDefault("Chat"); int valueOrDefault2 = item.ChatCounts.GetValueOrDefault("Cowork"); int valueOrDefault3 = item.ChatCounts.GetValueOrDefault("Code"); int num2 = valueOrDefault + valueOrDefault2 + valueOrDefault3; bool isToday = item.Date == today; ChatCountBars.Add(new DayBarItem { DateLabel = value.ToString("M/d"), DayLabel = value.ToString("ddd").Substring(0, 1), Value = num2, BarHeight = ((num2 > 0) ? ((double)num2 * 66.0 / (double)num) : 2.0), ValueLabel = ((num2 > 0) ? num2.ToString() : ""), ToolTipText = $"{item.Date} ({value:ddd})\nChat: {valueOrDefault}회 · Cowork: {valueOrDefault2}회 · Code: {valueOrDefault3}회", IsToday = isToday }); } } private void BuildTokenUsageChart(List stats, string today) { List list = stats.TakeLast(14).ToList(); long num = list.Max((DailyUsageStats s) => s.TotalTokens); if (num == 0) { num = 1L; } TokenUsageBars.Clear(); foreach (DailyUsageStats item in list) { DateTime result; DateTime value = (DateTime.TryParse(item.Date, out result) ? result : DateTime.MinValue); bool isToday = item.Date == today; long totalTokens = item.TotalTokens; TokenUsageBars.Add(new DayBarItem { DateLabel = value.ToString("M/d"), DayLabel = value.ToString("ddd").Substring(0, 1), Value = (int)Math.Min(totalTokens, 2147483647L), BarHeight = ((totalTokens > 0) ? ((double)totalTokens * 66.0 / (double)num) : 2.0), ValueLabel = ((totalTokens > 0) ? FormatTokens(totalTokens) : ""), ToolTipText = $"{item.Date} ({value:ddd})\n프롬프트: {item.PromptTokens:N0} · 완료: {item.CompletionTokens:N0} · 합계: {item.TotalTokens:N0}", IsToday = isToday }); } } private static string FormatTokens(long tokens) { if (1 == 0) { } string result = ((tokens >= 1000000) ? $"{(double)tokens / 1000000.0:F1}M" : ((tokens < 1000) ? tokens.ToString() : $"{(double)tokens / 1000.0:F1}K")); if (1 == 0) { } return result; } private void BuildWeekdayAvgChart(List stats) { Dictionary> groups = new Dictionary>(); DayOfWeek[] values = Enum.GetValues(); foreach (DayOfWeek key in values) { groups[key] = new List(); } foreach (DailyUsageStats stat in stats) { if (DateTime.TryParse(stat.Date, out var result)) { groups[result.DayOfWeek].Add(stat.LauncherOpens); } } DayOfWeek[] array = new DayOfWeek[7] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }; double num = array.Max((DayOfWeek dow) => (groups[dow].Count > 0) ? groups[dow].Average() : 0.0); if (num < 1.0) { num = 1.0; } WeekdayAvgBars.Clear(); DayOfWeek[] array2 = array; foreach (DayOfWeek dayOfWeek in array2) { List list = groups[dayOfWeek]; double num3 = ((list.Count > 0) ? list.Average() : 0.0); WeekdayAvgBars.Add(new DayBarItem { DateLabel = "", DayLabel = GetKoreanDayOfWeek(dayOfWeek), Value = (int)Math.Round(num3), BarHeight = ((num3 > 0.0) ? (num3 * 66.0 / num) : 2.0), ValueLabel = ((num3 > 0.0) ? num3.ToString("F1") : ""), ToolTipText = $"{GetKoreanDayOfWeek(dayOfWeek)}요일 평균 {num3:F1}회 ({list.Count}일 데이터)", IsToday = (DateTime.Today.DayOfWeek == dayOfWeek) }); } } private void BuildTabChatRatios(List stats) { int num = 0; int num2 = 0; int num3 = 0; foreach (DailyUsageStats stat in stats) { num += stat.ChatCounts.GetValueOrDefault("Chat"); num2 += stat.ChatCounts.GetValueOrDefault("Cowork"); num3 += stat.ChatCounts.GetValueOrDefault("Code"); } int num4 = num + num2 + num3; if (num4 == 0) { num4 = 1; } TabChatRatios.Clear(); TabChatRatios.Add(new TabRatioItem { TabName = "Chat", Count = num, Percentage = 100.0 * (double)num / (double)num4, BarWidth = 300.0 * (double)num / (double)num4, PercentLabel = $"{100.0 * (double)num / (double)num4:F0}% ({num}회)", Color = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#818CF8")) }); TabChatRatios.Add(new TabRatioItem { TabName = "Cowork", Count = num2, Percentage = 100.0 * (double)num2 / (double)num4, BarWidth = 300.0 * (double)num2 / (double)num4, PercentLabel = $"{100.0 * (double)num2 / (double)num4:F0}% ({num2}회)", Color = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#10B981")) }); TabChatRatios.Add(new TabRatioItem { TabName = "Code", Count = num3, Percentage = 100.0 * (double)num3 / (double)num4, BarWidth = 300.0 * (double)num3 / (double)num4, PercentLabel = $"{100.0 * (double)num3 / (double)num4:F0}% ({num3}회)", Color = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F59E0B")) }); } private void BuildTokenRatio(List stats) { long num = stats.Sum((DailyUsageStats s) => s.PromptTokens); long num2 = stats.Sum((DailyUsageStats s) => s.CompletionTokens); long num3 = num + num2; if (num3 == 0) { num3 = 1L; } PromptBarWidth = 400.0 * (double)num / (double)num3; CompletionBarWidth = 400.0 * (double)num2 / (double)num3; PromptTokenLabel = $"입력 {FormatTokens(num)} ({100.0 * (double)num / (double)num3:F0}%)"; CompletionTokenLabel = $"출력 {FormatTokens(num2)} ({100.0 * (double)num2 / (double)num3:F0}%)"; TokenRatioSummary = $"30일 합계 입력 {FormatTokens(num)} + 출력 {FormatTokens(num2)} = {FormatTokens(num + num2)}"; } private void BuildTopCommands(List stats) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (DailyUsageStats stat in stats) { foreach (var (key, num2) in stat.CommandUsage) { dictionary.TryGetValue(key, out var value); dictionary[key] = value + num2; } } List> list = dictionary.OrderByDescending((KeyValuePair kv) => kv.Value).Take(10).ToList(); int num3 = ((list.Count <= 0) ? 1 : list[0].Value); TopCommands.Clear(); for (int num4 = 0; num4 < list.Count; num4++) { TopCommands.Add(new CommandStatItem { Rank = num4 + 1, Command = list[num4].Key, Count = list[num4].Value, BarWidth = 200.0 * (double)list[num4].Value / (double)num3 }); } } private void BuildSummaryCards(List stats, string today) { TodayDate = DateTime.Today.ToString("yyyy년 M월 d일"); DailyUsageStats dailyUsageStats = stats.FirstOrDefault((DailyUsageStats s) => s.Date == today); TodaySummary = ((dailyUsageStats != null) ? $"{dailyUsageStats.LauncherOpens}회 호출 · {FormatActiveTime(dailyUsageStats.ActiveSeconds)}" : "데이터 없음"); DailyUsageStats dailyUsageStats2 = stats.OrderByDescending((DailyUsageStats s) => s.LauncherOpens).FirstOrDefault(); if (dailyUsageStats2 != null && dailyUsageStats2.LauncherOpens > 0) { PeakDate = (DateTime.TryParse(dailyUsageStats2.Date, out var result) ? result.ToString("yyyy년 M월 d일") : dailyUsageStats2.Date); PeakDaySummary = $"{dailyUsageStats2.LauncherOpens}회 호출"; } else { PeakDate = ""; PeakDaySummary = "기록 없음"; } DateTime weekStart = DateTime.Today.AddDays(0 - DateTime.Today.DayOfWeek); DateTime result2; int value = stats.Where((DailyUsageStats s) => DateTime.TryParse(s.Date, out result2) && result2 >= weekStart).Sum((DailyUsageStats s) => s.LauncherOpens); WeekSummary = $"이번 주 {value}회"; int value2 = stats.Sum((DailyUsageStats s) => s.LauncherOpens); TotalOpensSummary = $"30일 합계 {value2}회"; int value3 = stats.Sum((DailyUsageStats s) => s.ChatCounts.Values.Sum()); long tokens = stats.Sum((DailyUsageStats s) => s.TotalTokens); int value4 = dailyUsageStats?.ChatCounts.Values.Sum() ?? 0; long tokens2 = dailyUsageStats?.TotalTokens ?? 0; AgentSummary = $"오늘 {value4}회 대화 · {FormatTokens(tokens2)} 토큰 | 30일 합계 {value3}회 · {FormatTokens(tokens)} 토큰"; } private void BuildTopFavorites() { TopFavorites.Clear(); string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favorites.json"); if (!File.Exists(path)) { HasFavorites = false; return; } try { JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; List source = JsonSerializer.Deserialize>(File.ReadAllText(path), options) ?? new List(); List list = source.Take(6).ToList(); if (list.Count == 0) { HasFavorites = false; return; } for (int i = 0; i < list.Count; i++) { FavFileEntry favFileEntry = list[i]; string path2 = Environment.ExpandEnvironmentVariables(favFileEntry.Path); bool flag = Directory.Exists(path2); bool exists = flag || File.Exists(path2); TopFavorites.Add(new FavoriteStatItem { Rank = i + 1, Name = favFileEntry.Name, Path = favFileEntry.Path, Icon = (flag ? "\ue8b7" : "\ue8a5"), Exists = exists }); } HasFavorites = TopFavorites.Count > 0; } catch { HasFavorites = false; } } private static string GetKoreanDayOfWeek(DayOfWeek dow) { if (1 == 0) { } string result = dow switch { DayOfWeek.Monday => "월", DayOfWeek.Tuesday => "화", DayOfWeek.Wednesday => "수", DayOfWeek.Thursday => "목", DayOfWeek.Friday => "금", DayOfWeek.Saturday => "토", _ => "일", }; if (1 == 0) { } return result; } private static string FormatActiveTime(int totalSeconds) { if (totalSeconds <= 0) { return ""; } int num = totalSeconds / 3600; int num2 = totalSeconds % 3600 / 60; if (num > 0 && num2 > 0) { return $"{num}시간 {num2}분"; } if (num <= 0) { if (num2 <= 0) { return $"{totalSeconds}초"; } return $"{num2}분"; } return $"{num}시간"; } protected void OnPropertyChanged([CallerMemberName] string? n = null) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); } }