-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add homepage stats widget with analytics dashboard and mock data support #3839
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add homepage stats widget with analytics dashboard and mock data support #3839
Conversation
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.
There was a problem hiding this 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.
| 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>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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);
});
});
}| final wordsCount = _calculateTotalWords(conversationProvider.conversations); | |
| final wordsCount = conversationProvider.totalWordsCount; |
References
- 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.
There was a problem hiding this comment.
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.
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]>
|
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 |
|
closing it for now since there has been no update for 2 weeks, pls feel free to reopen it once its ready |
|
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:
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! 💜 |
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)
Analytics Dashboard
Mock Data Support
Added
DevConstants.useMockDatatoggle inlib/utils/constants.dartfor simulator/emulator testing:BEHAVIOR SUMMARY:
Files Changed
lib/pages/home/widgets/stats_widget.dart- Stats banner component with card UI and chevronlib/pages/home/widgets/stats_detail_sheet.dart- Analytics dashboard modal (938 lines)lib/utils/constants.dart- Mock data configuration togglelib/pages/home/page.dart- Integrated stats widgetlib/providers/conversation_provider.dart- Added mock conversation datalib/providers/memories_provider.dart- Added mock memories dataScreenshots
Testing
useMockData = trueuseMockData = falseImpact
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.