Skip to content

Conversation

@arnavlohiya
Copy link

@arnavlohiya arnavlohiya commented Dec 19, 2025

Summary

Implements homepage statistics display showing conversation count, memory count, and word count with a detailed analytics dashboard.

Closes #3469

Features Implemented

Stats Banner (Homepage)

  • Displays conversations count, memories count, and words count
  • Card-style design with rounded corners and chevron indicator
  • Tappable banner opens detailed analytics modal
  • Clean, minimal design that fits homepage layout

Analytics Dashboard

  • TikTok-style vertical swipe interface with card navigation
  • Weekly trends: Week-over-week comparison with percentage changes
  • Daily cadence: Bar chart showing conversations per day
  • Word cloud: Most-used words with 145+ stopwords filtered
  • Category breakdown: Pie chart showing conversation categories
  • Time-of-day analysis: When users talk most (Morning/Afternoon/Evening/Night)
  • Streak tracking: Consecutive days and best day identification
  • Efficiency metrics: WPM, memories per conversation, avg words

Mock Data Support

Added DevConstants.useMockData toggle in lib/utils/constants.dart for simulator/emulator testing:

  • ✅ Enables UI testing without microphone/backend access
  • ✅ Includes 7 realistic mock conversations (~120 words)
  • ✅ Includes 15 mock memories across categories
  • ✅ Helps contributors test features without physical devices
  • ❌ Note: AI Chat won't have memory context with mock data (backend queries its own database)

BEHAVIOR SUMMARY:

  • useMockData=true + NO device connected = Mock data shown ✅
  • useMockData=true + Device connected = Real device data shown ✅
  • useMockData=false = Always uses real API/device data ✅

Files Changed

  • lib/pages/home/widgets/stats_widget.dart - Stats banner component with card UI and chevron
  • lib/pages/home/widgets/stats_detail_sheet.dart - Analytics dashboard modal (938 lines)
  • lib/utils/constants.dart - Mock data configuration toggle
  • lib/pages/home/page.dart - Integrated stats widget
  • lib/providers/conversation_provider.dart - Added mock conversation data
  • lib/providers/memories_provider.dart - Added mock memories data

Screenshots

Screenshot_20251219_151608 Screenshot_20251219_151647 Screenshot_20251219_151709 Screenshot_20251219_151720

Testing

  • Stats display correctly on homepage
  • All three metrics (conversations, memories, words) calculate properly
  • Card UI with chevron indicates interactivity
  • Modal opens smoothly on banner tap
  • Vertical swipe navigation works between analytics cards
  • Mock data loads when useMockData = true
  • Real data works when useMockData = false
  • Word cloud filters stopwords correctly (145+ words)
  • Charts render with accurate data
  • No compilation errors

Impact

This feature provides users with visible progress metrics to "feel the value from the app everytime they open it" (as requested in #3469), encouraging engagement and sharing.

Implements a stats banner on the homepage showing conversation count, memory count, and word count. Users can tap the banner to open a detailed analytics dashboard with:
- Weekly trends and momentum tracking
- Daily conversation cadence charts
- Word cloud with comprehensive stopword filtering
- Category breakdown with pie chart visualization
- Time-of-day analysis and streak tracking
- Efficiency metrics (WPM, memories per conversation)

Also adds mock data support for testing without microphone/backend access via DevConstants.useMockData flag in utils/constants.dart. This enables UI/UX testing on simulators and helps contributors test features without physical devices.
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable homepage statistics widget and a detailed analytics dashboard, significantly enhancing user engagement. The implementation of mock data for testing is a commendable practice. My review focuses on improving performance and type safety in the new analytics features. I've identified some critical and high-severity issues where expensive calculations are performed inefficiently and unsafe type casting could lead to runtime errors. Addressing these by moving logic to providers and utilizing data classes will make the new features more robust and performant.

Comment on lines 286 to 297
final dailyCounts = stats['dailyCounts'] as Map<String, int>;
final topCategories = stats['topCategories'] as List<MapEntry<String, int>>;
final streak = stats['streak'] as int;
final bestDay = stats['bestDay'] as MapEntry<String, int>?;
final topWords = stats['topWords'] as List<MapEntry<String, int>>;
final timeOfDayBuckets = stats['timeOfDayBuckets'] as Map<String, int>;
final avgWordsPerConvo = stats['avgWordsPerConvo'] as double;
final avgWpm = stats['avgWpm'] as double;
final longestDurationMinutes = stats['longestDurationMinutes'] as double;
final maxWordsInConvo = stats['maxWordsInConvo'] as int;
final memoriesPerConvo = stats['memoriesPerConvo'] as double;
final categoryMomentum = stats['categoryMomentum'] as List<_CategoryMomentum>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using Map<String, dynamic> and then casting with as is not type-safe and can lead to runtime crashes if a key is misspelled, a value is missing, or the type is incorrect. This makes the code brittle and hard to maintain.

A much safer and more robust approach is to define a dedicated data class to hold the statistics. This provides compile-time type safety, enables autocompletion in the IDE, and makes the code easier to read and reason about.

Here's an example of what the data class could look like:

class _AnalyticsStats {
  final int thisWeekConvos;
  final int lastWeekConvos;
  final int thisWeekWords;
  final int lastWeekWords;
  final Map<String, int> dailyCounts;
  final List<MapEntry<String, int>> topCategories;
  final int streak;
  final MapEntry<String, int>? bestDay;
  final List<MapEntry<String, int>> topWords;
  final Map<String, int> timeOfDayBuckets;
  final double avgWordsPerConvo;
  final double avgWpm;
  final double longestDurationMinutes;
  final int maxWordsInConvo;
  final double memoriesPerConvo;
  final List<_CategoryMomentum> categoryMomentum;

  _AnalyticsStats({
    required this.thisWeekConvos,
    required this.lastWeekConvos,
    required this.thisWeekWords,
    required this.lastWeekWords,
    required this.dailyCounts,
    required this.topCategories,
    required this.streak,
    this.bestDay,
    required this.topWords,
    required this.timeOfDayBuckets,
    required this.avgWordsPerConvo,
    required this.avgWpm,
    required this.longestDurationMinutes,
    required this.maxWordsInConvo,
    required this.memoriesPerConvo,
    required this.categoryMomentum,
  });
}

The _buildStats method should be updated to return an instance of this class, and the build method can then access the properties safely (e.g., stats.dailyCounts).


@override
Widget build(BuildContext context) {
final stats = _buildStats();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _buildStats() method is called directly within the build() method. This is inefficient as _buildStats() performs expensive computations over all conversations, and the build() method can be called multiple times by the framework, leading to unnecessary recalculations. To improve performance, these stats should be computed once when the widget is initialized and stored in the state.

I recommend calculating the stats in initState() and storing them in a late final variable. This ensures the heavy computation runs only once when the sheet is first built.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solved in the new Commit

builder: (context, conversationProvider, memoriesProvider, child) {
final conversationsCount = conversationProvider.conversations.length;
final memoriesCount = memoriesProvider.memories.length;
final wordsCount = _calculateTotalWords(conversationProvider.conversations);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _calculateTotalWords method is executed within the build method, which is inefficient as it iterates over all conversations on every rebuild. This can lead to performance degradation, especially with a large number of conversations. This logic should be moved to the ConversationProvider to be calculated once and cached, ensuring the UI remains responsive.

You should remove the _calculateTotalWords method from this widget and add a totalWordsCount getter to ConversationProvider.

// In ConversationProvider:
int get totalWordsCount {
  return conversations.fold(0, (sum, convo) {
    return sum + convo.transcriptSegments.fold(0, (sum, seg) {
      final trimmed = seg.text.trim();
      return sum + (trimmed.isEmpty ? 0 : trimmed.split(RegExp(r'\s+')).length);
    });
  });
}
Suggested change
final wordsCount = _calculateTotalWords(conversationProvider.conversations);
final wordsCount = conversationProvider.totalWordsCount;
References
  1. When separating UI and provider logic, provider methods should handle state management, while the UI layer is responsible for invoking subsequent asynchronous actions after the state has been modified.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update is required as the numbers need to change when conversations are updated in the data. Otherwise, the numbers would reflect old data while conversations are updated.

arnavlohiya and others added 7 commits December 19, 2025 19:21
Move expensive stats computation from build() to initState() to prevent
unnecessary recalculations. Stats are now computed once when the sheet
opens instead of on every rebuild (scrolling, dragging, etc.).

This addresses Gemini code review feedback about performance inefficiency.
Add conditional rendering to display StatsWidget only when on the home
tab (selectedIndex == 0). This prevents the stats widget from appearing
on other tabs like conversations and memories where it's not relevant.
Added comprehensive documentation to clarify that when a physical Omi device is connected via Bluetooth, it will override mock data by design. This is the intended behavior - mock data is for UI testing without a device.

Key additions:
- Explanation of WAL sync and device recording stream behavior
- Behavior summary table showing all three scenarios
- Clear indication this is intentional, not a bug

This helps developers understand why mock data "stops working" when they connect a real device.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Removed the height: 60 constraint from the SizedBox in StatsWidget to allow the widget to size itself naturally based on its content and padding. This provides better flexibility for the stats display across different screen sizes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit addresses two critical issues:

1. **Fix stats widget conversation count inconsistency**
   - Added filteredConversations getter in ConversationProvider
   - Updated StatsWidget to use filtered conversations instead of raw list
   - Ensures stats widget count matches the conversation list display
   - Respects all active filters (discarded, short, starred, date)

2. **Fix mock data not loading in simulator**
   - Added missing _preload() call in ConversationProvider constructor
   - Mock data now loads correctly when useMockData = true
   - Enables UI testing without connected Omi device

Files changed:
- app/lib/providers/conversation_provider.dart
- app/lib/pages/home/widgets/stats_widget.dart

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@beastoin beastoin marked this pull request as draft December 25, 2025 04:49
@beastoin
Copy link
Collaborator

wow btw keep it as draft until it's ready

you should seeking for the direct feedback from Aarav man (whom write the ticket)

thank you @arnavlohiya

@mdmohsin7
Copy link
Member

closing it for now since there has been no update for 2 weeks, pls feel free to reopen it once its ready

@mdmohsin7 mdmohsin7 closed this Jan 6, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

Hey @arnavlohiya 👋

Thank you so much for taking the time to contribute to Omi! We truly appreciate you putting in the effort to submit this pull request.

After careful review, we've decided not to merge this particular PR. Please don't take this personally — we genuinely try to merge as many contributions as possible, but sometimes we have to make tough calls based on:

  • Project standards — Ensuring consistency across the codebase
  • User needs — Making sure changes align with what our users need
  • Code best practices — Maintaining code quality and maintainability
  • Project direction — Keeping aligned with our roadmap and vision

Your contribution is still valuable to us, and we'd love to see you contribute again in the future! If you'd like feedback on how to improve this PR or want to discuss alternative approaches, please don't hesitate to reach out.

Thank you for being part of the Omi community! 💜

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

show stats on homepage like wispr flow

3 participants