cacheToStats method

NeomageStats cacheToStats(
  1. PersistedStatsCache cache,
  2. ProcessedStats? todayStats
)

Convert a PersistedStatsCache to NeomageStats by computing derived fields, optionally merging today's live stats.

Implementation

NeomageStats cacheToStats(
  PersistedStatsCache cache,
  ProcessedStats? todayStats,
) {
  // Merge cache with today's stats.
  final dailyActivityMap = <String, DailyActivity>{};
  for (final day in cache.dailyActivity) {
    dailyActivityMap[day.date] = day.copyWith();
  }
  if (todayStats != null) {
    for (final day in todayStats.dailyActivity) {
      final existing = dailyActivityMap[day.date];
      if (existing != null) {
        existing.messageCount += day.messageCount;
        existing.sessionCount += day.sessionCount;
        existing.toolCallCount += day.toolCallCount;
      } else {
        dailyActivityMap[day.date] = day.copyWith();
      }
    }
  }

  final dailyModelTokensMap = <String, Map<String, int>>{};
  for (final day in cache.dailyModelTokens) {
    dailyModelTokensMap[day.date] = Map<String, int>.from(day.tokensByModel);
  }
  if (todayStats != null) {
    for (final day in todayStats.dailyModelTokens) {
      final existing = dailyModelTokensMap[day.date];
      if (existing != null) {
        for (final entry in day.tokensByModel.entries) {
          existing[entry.key] = (existing[entry.key] ?? 0) + entry.value;
        }
      } else {
        dailyModelTokensMap[day.date] = Map<String, int>.from(
          day.tokensByModel,
        );
      }
    }
  }

  // Merge model usage.
  final modelUsage = Map<String, ModelUsage>.from(cache.modelUsage);
  if (todayStats != null) {
    for (final entry in todayStats.modelUsage.entries) {
      final existing = modelUsage[entry.key];
      if (existing != null) {
        modelUsage[entry.key] = existing.merge(entry.value);
      } else {
        modelUsage[entry.key] = entry.value;
      }
    }
  }

  // Merge hour counts.
  final hourCountsMap = <int, int>{};
  for (final entry in cache.hourCounts.entries) {
    hourCountsMap[entry.key] = entry.value;
  }
  if (todayStats != null) {
    for (final entry in todayStats.hourCounts.entries) {
      hourCountsMap[entry.key] =
          (hourCountsMap[entry.key] ?? 0) + entry.value;
    }
  }

  // Calculate derived stats.
  final dailyActivityArray = dailyActivityMap.values.toList()
    ..sort((a, b) => a.date.compareTo(b.date));
  final streaks = calculateStreaks(dailyActivityArray);

  final dailyModelTokens =
      dailyModelTokensMap.entries
          .map((e) => DailyModelTokens(date: e.key, tokensByModel: e.value))
          .toList()
        ..sort((a, b) => a.date.compareTo(b.date));

  final totalSessions =
      cache.totalSessions + (todayStats?.sessionStats.length ?? 0);
  final totalMessages =
      cache.totalMessages + (todayStats?.totalMessages ?? 0);

  SessionStats? longestSession = cache.longestSession;
  if (todayStats != null) {
    for (final session in todayStats.sessionStats) {
      if (longestSession == null ||
          session.duration > longestSession.duration) {
        longestSession = session;
      }
    }
  }

  String? firstSessionDate = cache.firstSessionDate;
  String? lastSessionDate;
  if (todayStats != null) {
    for (final session in todayStats.sessionStats) {
      if (firstSessionDate == null ||
          session.timestamp.compareTo(firstSessionDate) < 0) {
        firstSessionDate = session.timestamp;
      }
      if (lastSessionDate == null ||
          session.timestamp.compareTo(lastSessionDate) > 0) {
        lastSessionDate = session.timestamp;
      }
    }
  }
  if (lastSessionDate == null && dailyActivityArray.isNotEmpty) {
    lastSessionDate = dailyActivityArray.last.date;
  }

  final peakActivityDay = dailyActivityArray.isNotEmpty
      ? dailyActivityArray
            .reduce((m, d) => d.messageCount > m.messageCount ? d : m)
            .date
      : null;

  final peakActivityHour = hourCountsMap.isNotEmpty
      ? hourCountsMap.entries.reduce((m, e) => e.value > m.value ? e : m).key
      : null;

  final totalDays = (firstSessionDate != null && lastSessionDate != null)
      ? ((DateTime.parse(
              lastSessionDate,
            ).difference(DateTime.parse(firstSessionDate)).inDays) +
            1)
      : 0;

  final totalSpeculationTimeSavedMs =
      cache.totalSpeculationTimeSavedMs +
      (todayStats?.totalSpeculationTimeSavedMs ?? 0);

  Map<int, int>? shotDistribution;
  int? oneShotRate;
  if (shotStatsEnabled.value) {
    shotDistribution = Map<int, int>.from(cache.shotDistribution ?? {});
    if (todayStats?.shotDistribution != null) {
      for (final entry in todayStats!.shotDistribution!.entries) {
        shotDistribution[entry.key] =
            (shotDistribution[entry.key] ?? 0) + entry.value;
      }
    }
    final totalWithShots = shotDistribution.values.fold<int>(
      0,
      (s, n) => s + n,
    );
    oneShotRate = totalWithShots > 0
        ? ((shotDistribution[1] ?? 0) / totalWithShots * 100).round()
        : 0;
  }

  return NeomageStats(
    totalSessions: totalSessions,
    totalMessages: totalMessages,
    totalDays: totalDays,
    activeDays: dailyActivityMap.length,
    streaks: streaks,
    dailyActivity: dailyActivityArray,
    dailyModelTokens: dailyModelTokens,
    longestSession: longestSession,
    modelUsage: modelUsage,
    firstSessionDate: firstSessionDate,
    lastSessionDate: lastSessionDate,
    peakActivityDay: peakActivityDay,
    peakActivityHour: peakActivityHour,
    totalSpeculationTimeSavedMs: totalSpeculationTimeSavedMs,
    shotDistribution: shotDistribution,
    oneShotRate: oneShotRate,
  );
}