[FOSSOVERFLOW-26] per-user mood tracking, Maya insights & privacy layer, News API articles#9
[FOSSOVERFLOW-26] per-user mood tracking, Maya insights & privacy layer, News API articles#9Ashish-Kumar-Dash wants to merge 3 commits intoOpenLake:mainfrom
Conversation
WalkthroughThis PR introduces a medical data sharing consent flow for users before accessing the AI chat, adds a complete article reading system with dynamic content and News API integration, migrates mood tracking to Supabase with Maya insights analysis, and updates navigation logic accordingly while adding cross-platform sharing support. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant HomePage
participant MayaConsentPage
participant Supabase
participant AiChatPage
participant ChatService
User->>HomePage: Open app / Tap FAB
HomePage->>HomePage: Load patient profile
alt No consent record found
HomePage->>MayaConsentPage: Navigate (consent not given)
User->>MayaConsentPage: Select Yes/No to share medical data
MayaConsentPage->>Supabase: Save maya_data_consent & date
Supabase-->>MayaConsentPage: Confirm update
MayaConsentPage->>AiChatPage: Navigate with sharesMedicalData flag
else Consent record exists
HomePage->>AiChatPage: Navigate with sharesMedicalData value
end
AiChatPage->>AiChatPage: Initialize in initState
AiChatPage->>ChatService: Create with usePersonalData flag
ChatService->>ChatService: Build system prompt (respect consent)
User->>AiChatPage: Start chatting
sequenceDiagram
participant User
participant ArticlePage
participant ArticleService
participant NewsAPI
participant ArticleReaderPage
User->>ArticlePage: Open articles
ArticlePage->>ArticleService: fetchPopularArticles()
ArticleService->>NewsAPI: HTTP GET popular wellness articles
NewsAPI-->>ArticleService: JSON response
ArticleService->>ArticleService: Parse, clean HTML, validate
ArticleService-->>ArticlePage: List of Articles
ArticlePage->>ArticlePage: Render carousel & cache
User->>ArticlePage: Select category
ArticlePage->>ArticleService: fetchArticles(category)
ArticleService->>NewsAPI: HTTP GET category-specific articles
NewsAPI-->>ArticleService: JSON response
ArticleService-->>ArticlePage: List of Articles
ArticlePage->>ArticlePage: Render list & cache
User->>ArticlePage: Tap article tile
ArticlePage->>ArticleReaderPage: Navigate with Article object
ArticleReaderPage->>User: Display collapsible AppBar, content
User->>ArticleReaderPage: Tap share button
ArticleReaderPage->>ArticleReaderPage: Trigger share_plus
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
flutter_app/lib/pages/mood_tracker.dart (1)
1277-1505:⚠️ Potential issue | 🔴 CriticalCritical: Orphaned code outside class scope will cause compilation error.
Lines 1277-1505 contain a
buildmethod with@overrideannotation that exists outside of any class. This is invalid Dart syntax and will prevent the file from compiling. This appears to be leftover/duplicate code from a previous version that wasn't properly removed.The actual working
buildmethod is correctly defined at lines 813-855 within_MoodTrackerPageState.🗑️ Remove the orphaned code block
Delete lines 1277-1505 entirely. The entire block starting with
@override Widget build(BuildContext context)through the final closing brace is dead code.- - - `@override` - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Mood Tracker"), - centerTitle: true, - elevation: 4, - ), - body: SafeArea( - // ... entire block through line 1505 - ), - ); - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/pages/mood_tracker.dart` around lines 1277 - 1505, Remove the orphaned `@override` Widget build(BuildContext context) method block that exists outside any class (the duplicate/dead build implementation) so only the correct build method in _MoodTrackerPageState remains; delete the entire standalone build method and its enclosing braces, then verify class braces remain balanced and the file compiles (check for any leftover dangling braces or duplicate symbols after removing the orphaned build).
🧹 Nitpick comments (8)
flutter_app/lib/pages/mood_tracker.dart (1)
216-232: Add timeout to HTTP request to prevent indefinite hangs.The
http.postcall has no timeout configured. If the OpenRouter API is slow or unresponsive, the request could hang indefinitely, leaving the user stuck with the loading state.⏱️ Proposed fix with timeout
final response = await http.post( Uri.parse('https://openrouter.ai/api/v1/chat/completions'), headers: { 'Authorization': 'Bearer $apiKey', 'Content-Type': 'application/json', 'HTTP-Referer': 'https://getwelplus.app', 'X-Title': 'GetWel+', }, body: jsonEncode({ 'model': 'stepfun/step-3.5-flash:free', 'messages': [ {'role': 'user', 'content': prompt} ], 'temperature': 0.7, 'max_tokens': 300, }), - ); + ).timeout( + const Duration(seconds: 30), + onTimeout: () => throw Exception('Request timed out'), + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/pages/mood_tracker.dart` around lines 216 - 232, The http.post call to 'https://openrouter.ai/api/v1/chat/completions' currently has no timeout and can hang; update the request where http.post(...) is invoked (the snippet using Uri.parse and assigning to response) to apply a timeout (e.g., using .timeout(Duration(seconds: 10))) and wrap the await in a try/catch that handles TimeoutException (and other exceptions) to stop the loading state and surface an error to the user; ensure you still inspect the resulting response if successful and set response/error handling in the same function that currently awaits and uses the response variable.flutter_app/lib/services/article_service.dart (1)
42-61: Consider using a proper HTML entity decoder.The manual entity replacement is limited. Dart's
dart:convertdoesn't have HTML decoding, but thehtml_unescapepackage orhtmlpackage provides comprehensive decoding if you encounter more entities.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/services/article_service.dart` around lines 42 - 61, The manual entity replacements in _stripHtml are incomplete; switch to a proper HTML entity decoder (e.g., html_unescape's HtmlUnescape or the html package) to robustly decode entities: keep the regex tag-removal step, then call the decoder on the cleaned string instead of the long chain of replaceAll calls, add the necessary import for the chosen package, and return the trimmed decoded result from _stripHtml.flutter_app/lib/pages/article_page.dart (2)
129-134: Consider parallel refresh for better UX.Currently, popular articles are awaited before category articles. Running them in parallel would reduce refresh time.
Proposed improvement
onRefresh: () async { - await _loadPopularArticles(); _categoryArticles.clear(); // clear cache - await _loadCategoryArticles(currentCategory); + await Future.wait([ + _loadPopularArticles(), + _loadCategoryArticles(currentCategory), + ]); },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/pages/article_page.dart` around lines 129 - 134, The refresh currently awaits _loadPopularArticles() before clearing _categoryArticles and calling _loadCategoryArticles(currentCategory), causing unnecessary sequential delay; change to run them in parallel (for example use Future.wait) so both _loadPopularArticles() and _loadCategoryArticles(currentCategory) execute concurrently, but still clear _categoryArticles() before starting the category load to avoid stale data; update the RefreshIndicator onRefresh handler to kick off both futures and await their combined completion.
355-357: Replace deprecatedwithOpacitywithwithValues.
withOpacity()is deprecated in Flutter due to precision loss when handling floating-point alpha values; usewithValues(alpha: ...)instead.Suggested fix
border: Border.all( - color: scheme.outlineVariant.withOpacity(0.5), + color: scheme.outlineVariant.withValues(alpha: 0.5), ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/pages/article_page.dart` around lines 355 - 357, The Border.all call uses scheme.outlineVariant.withOpacity(0.5) which relies on the deprecated withOpacity; replace it by calling withValues(alpha: 0.5) on the same Color instance (i.e., change scheme.outlineVariant.withOpacity(0.5) to scheme.outlineVariant.withValues(alpha: 0.5)) so the Border.all color uses the new API without precision-loss warnings.flutter_app/lib/widgets/article_card.dart (2)
5-6:imagePathfield is currently unused.Per context snippet 1, the only caller (
article_page.dart) always passesimageUrland neverimagePath. The asset image fallback at lines 156-162 is dead code. This is acceptable if you plan to support local assets in the future, but consider documenting the intent or removing if not needed.Also applies to: 15-16, 143-165
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/widgets/article_card.dart` around lines 5 - 6, The imagePath field and the asset-image fallback in ArticleCard are currently dead code because callers (e.g., article_page.dart) always pass imageUrl; either remove imagePath and the fallback branch to avoid unused state, or make the fallback active by using imagePath when imageUrl is null: update the ArticleCard constructor (remove imagePath or mark as required/nullable accordingly), delete the unused imagePath property and the asset-image rendering lines, or alter the image widget code to check imageUrl first and then use imagePath (the local asset) as the fallback so the asset branch (lines rendering the asset image) is actually used.
33-33: Replace deprecatedwithOpacitywithwithValues(alpha: …).Lines 33, 61, and 78 use the deprecated
Color.withOpacity()method (deprecated since Flutter 3.27). Replace withwithValues(alpha: ...):Suggested fixes
- shadowColor: Colors.black.withOpacity(0.15), + shadowColor: Colors.black.withValues(alpha: 0.15),- Colors.black.withOpacity(0.3), + Colors.black.withValues(alpha: 0.3),- color: Colors.black.withOpacity(0.6), + color: Colors.black.withValues(alpha: 0.6),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/widgets/article_card.dart` at line 33, Replace deprecated Color.withOpacity(...) calls in the ArticleCard widget with Color.withValues(alpha: ...). Locate the Color.withOpacity usages inside the ArticleCard class (e.g., the shadowColor assignment and the two other occurrences) and change each call to pass an integer alpha: compute alpha as (opacity * 255).round() and use that value in withValues(alpha: computedAlpha). Ensure you update all three occurrences so they use the new API consistently.flutter_app/lib/pages/article_reader_page.dart (2)
226-231: Silent failure when URL cannot be launched.If
canLaunchUrlreturnsfalse, the user receives no feedback. Consider showing a snackbar or toast to inform the user.Proposed fix
- Future<void> _openInBrowser(String url) async { + Future<void> _openInBrowser(String url, [BuildContext? ctx]) async { final uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); + } else if (ctx != null && ctx.mounted) { + ScaffoldMessenger.of(ctx).showSnackBar( + const SnackBar(content: Text('Could not open link')), + ); } }Then pass
contextfrom the call sites (lines 84, 204).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/pages/article_reader_page.dart` around lines 226 - 231, The _openInBrowser function currently silently does nothing when canLaunchUrl(uri) is false; update _openInBrowser to accept a BuildContext parameter, and when canLaunchUrl returns false (or launching throws), show a user-visible message (e.g., ScaffoldMessenger.of(context).showSnackBar with a helpful text) and handle exceptions by also showing the snack; then update all call sites that invoke _openInBrowser (the places where it was called without context) to pass the current BuildContext so the snackbar can be displayed.
62-63: Replace deprecatedwithOpacitywithwithValues.
Color.withOpacityis deprecated in Flutter 3.27+. Replace withColor.withValues(alpha: ...)using the same opacity value.Suggested fix
- Colors.black.withOpacity(0.7), + Colors.black.withValues(alpha: 0.7),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/pages/article_reader_page.dart` around lines 62 - 63, The Color.withOpacity usage is deprecated; replace the call to Colors.black.withOpacity(0.7) with Colors.black.withValues(alpha: 0.7) (or the equivalent alpha value) so the code uses Color.withValues instead of withOpacity; update any similar occurrences in article_reader_page.dart where withOpacity is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@flutter_app/lib/pages/ai_chat.dart`:
- Around line 6-8: The constructor for AiChatPage currently defaults
sharesMedicalData to true which makes consent opt-out; change the constructor to
make consent opt-in by either (a) setting the default to false in AiChatPage’s
constructor signature (const AiChatPage({super.key, this.sharesMedicalData =
false});) or (b) require the flag explicitly (const AiChatPage({super.key,
required this.sharesMedicalData});) and update all call sites that construct
AiChatPage to pass the appropriate value; ensure the final field final bool
sharesMedicalData remains unchanged.
In `@flutter_app/lib/pages/homepage.dart`:
- Around line 62-94: patientProfile is used for both "not-yet-loaded" and
"no-consent", causing incorrect routing; add a boolean state like
isProfileLoaded (managed in _loadPatientProfile around the Supabase call) and
treat null patientProfile as "unknown" until isProfileLoaded is true, and update
_openMayaChat to await Navigator.push(...) when showing MayaConsentPage so it
returns the user's choice (true/false) which you then use to setState
patientProfile['maya_data_consent'] or call _loadPatientProfile() to refresh;
also ensure when navigating to AiChatPage you pass sharesMedicalData: consent ==
true only after confirming consent is non-null and isProfileLoaded is true.
In `@flutter_app/lib/pages/maya_consent_page.dart`:
- Around line 136-158: The privacy blurb in MayaConsentPage currently
overpromises—update the Text inside the Container (the string that starts with
'Your data stays private & encrypted...') to explicitly state that opting in
will share profile-derived context with a third‑party chat provider (referencing
ChatService where profile/context is sent) and remove the blanket "private &
encrypted" and "change anytime in settings" claims since homepage settings to
revoke this are not implemented; change the copy to a clear consent statement
(e.g., mention third‑party processing and how to revoke when settings are
available) by editing the Text in the Container in MayaConsentPage and ensure
any matching hardcoded strings are updated accordingly.
In `@flutter_app/lib/pages/mood_tracker.dart`:
- Around line 234-245: The current parsing of jsonDecode(response.body) assumes
data['choices'][0]['message']['content'] exists and is a String which can crash;
wrap the parse in a try/catch and defensively validate the structure: ensure the
decoded value is a Map, that data['choices'] is a List with at least one
element, that the first element is a Map with 'message' Map containing a String
'content'. If validation fails, set _mayaInsight to a safe fallback message (or
extracted error text from the response) and set _loadingMaya = false inside
setState (checking mounted), and for non-200 responses include response.body in
the thrown Exception or logged error so failures are observable; use the
existing symbols (response, jsonDecode, _mayaInsight, _loadingMaya, mounted,
setState) when implementing these checks.
In `@flutter_app/lib/services/article_service.dart`:
- Line 121: Add a finite timeout to the HTTP requests in fetchArticles and
fetchPopularArticles by replacing the raw http.get(uri) calls with a timed
request (e.g., http.get(uri).timeout(Duration(seconds: X))) and handle
TimeoutException (import dart:async) to return a controlled error/response
instead of hanging; ensure both functions reference their existing http.get
calls and catch/handle TimeoutException consistently (logging or throwing a
descriptive exception) so callers receive a predictable failure on network
timeouts.
- Around line 70-83: The formattedDate getter produces negative intervals when
publishedAt is in the future; update formattedDate to detect if
publishedAt.isAfter(now) and handle that case (e.g., return 'Just now' or a
future-aware string) instead of using negative diff values. Locate the
formattedDate getter and add a guard that checks publishedAt against
DateTime.now(), then return a safe value or clamp the Duration to zero before
computing '${diff.inMinutes}m ago', '${diff.inHours}h ago', etc., ensuring no
negative numbers are shown.
In `@flutter_app/lib/services/chat_service.dart`:
- Around line 64-66: The constructor currently always calls
_initializeWithProfile(), causing patient_profiles to be loaded even when
_usePersonalData is false; modify the ChatService constructor (or add a guard at
the start of _initializeWithProfile()) to short-circuit and return immediately
when _usePersonalData is false, and ensure refreshProfile() also checks
_usePersonalData before fetching the patient_profiles row so no sensitive
profile data is read into memory for opted-out users.
---
Outside diff comments:
In `@flutter_app/lib/pages/mood_tracker.dart`:
- Around line 1277-1505: Remove the orphaned `@override` Widget build(BuildContext
context) method block that exists outside any class (the duplicate/dead build
implementation) so only the correct build method in _MoodTrackerPageState
remains; delete the entire standalone build method and its enclosing braces,
then verify class braces remain balanced and the file compiles (check for any
leftover dangling braces or duplicate symbols after removing the orphaned
build).
---
Nitpick comments:
In `@flutter_app/lib/pages/article_page.dart`:
- Around line 129-134: The refresh currently awaits _loadPopularArticles()
before clearing _categoryArticles and calling
_loadCategoryArticles(currentCategory), causing unnecessary sequential delay;
change to run them in parallel (for example use Future.wait) so both
_loadPopularArticles() and _loadCategoryArticles(currentCategory) execute
concurrently, but still clear _categoryArticles() before starting the category
load to avoid stale data; update the RefreshIndicator onRefresh handler to kick
off both futures and await their combined completion.
- Around line 355-357: The Border.all call uses
scheme.outlineVariant.withOpacity(0.5) which relies on the deprecated
withOpacity; replace it by calling withValues(alpha: 0.5) on the same Color
instance (i.e., change scheme.outlineVariant.withOpacity(0.5) to
scheme.outlineVariant.withValues(alpha: 0.5)) so the Border.all color uses the
new API without precision-loss warnings.
In `@flutter_app/lib/pages/article_reader_page.dart`:
- Around line 226-231: The _openInBrowser function currently silently does
nothing when canLaunchUrl(uri) is false; update _openInBrowser to accept a
BuildContext parameter, and when canLaunchUrl returns false (or launching
throws), show a user-visible message (e.g.,
ScaffoldMessenger.of(context).showSnackBar with a helpful text) and handle
exceptions by also showing the snack; then update all call sites that invoke
_openInBrowser (the places where it was called without context) to pass the
current BuildContext so the snackbar can be displayed.
- Around line 62-63: The Color.withOpacity usage is deprecated; replace the call
to Colors.black.withOpacity(0.7) with Colors.black.withValues(alpha: 0.7) (or
the equivalent alpha value) so the code uses Color.withValues instead of
withOpacity; update any similar occurrences in article_reader_page.dart where
withOpacity is used.
In `@flutter_app/lib/pages/mood_tracker.dart`:
- Around line 216-232: The http.post call to
'https://openrouter.ai/api/v1/chat/completions' currently has no timeout and can
hang; update the request where http.post(...) is invoked (the snippet using
Uri.parse and assigning to response) to apply a timeout (e.g., using
.timeout(Duration(seconds: 10))) and wrap the await in a try/catch that handles
TimeoutException (and other exceptions) to stop the loading state and surface an
error to the user; ensure you still inspect the resulting response if successful
and set response/error handling in the same function that currently awaits and
uses the response variable.
In `@flutter_app/lib/services/article_service.dart`:
- Around line 42-61: The manual entity replacements in _stripHtml are
incomplete; switch to a proper HTML entity decoder (e.g., html_unescape's
HtmlUnescape or the html package) to robustly decode entities: keep the regex
tag-removal step, then call the decoder on the cleaned string instead of the
long chain of replaceAll calls, add the necessary import for the chosen package,
and return the trimmed decoded result from _stripHtml.
In `@flutter_app/lib/widgets/article_card.dart`:
- Around line 5-6: The imagePath field and the asset-image fallback in
ArticleCard are currently dead code because callers (e.g., article_page.dart)
always pass imageUrl; either remove imagePath and the fallback branch to avoid
unused state, or make the fallback active by using imagePath when imageUrl is
null: update the ArticleCard constructor (remove imagePath or mark as
required/nullable accordingly), delete the unused imagePath property and the
asset-image rendering lines, or alter the image widget code to check imageUrl
first and then use imagePath (the local asset) as the fallback so the asset
branch (lines rendering the asset image) is actually used.
- Line 33: Replace deprecated Color.withOpacity(...) calls in the ArticleCard
widget with Color.withValues(alpha: ...). Locate the Color.withOpacity usages
inside the ArticleCard class (e.g., the shadowColor assignment and the two other
occurrences) and change each call to pass an integer alpha: compute alpha as
(opacity * 255).round() and use that value in withValues(alpha: computedAlpha).
Ensure you update all three occurrences so they use the new API consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 673d31c9-9729-4c95-89ed-797b100a5180
⛔ Files ignored due to path filters (1)
flutter_app/pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (14)
flutter_app/lib/pages/ai_chat.dartflutter_app/lib/pages/article_page.dartflutter_app/lib/pages/article_reader_page.dartflutter_app/lib/pages/homepage.dartflutter_app/lib/pages/maya_consent_page.dartflutter_app/lib/pages/mood_tracker.dartflutter_app/lib/pages/profile_page.dartflutter_app/lib/services/article_service.dartflutter_app/lib/services/chat_service.dartflutter_app/lib/widgets/article_card.dartflutter_app/macos/Flutter/GeneratedPluginRegistrant.swiftflutter_app/pubspec.yamlflutter_app/windows/flutter/generated_plugin_registrant.ccflutter_app/windows/flutter/generated_plugins.cmake
| final bool sharesMedicalData; | ||
|
|
||
| const AiChatPage({super.key, this.sharesMedicalData = true}); |
There was a problem hiding this comment.
Make the consent flag opt-in by default.
Defaulting sharesMedicalData to true weakens the privacy gate: any caller that forgets to pass the new argument will silently enable profile-based personalization. For this flow, default this to false or require it explicitly in the constructor.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/pages/ai_chat.dart` around lines 6 - 8, The constructor for
AiChatPage currently defaults sharesMedicalData to true which makes consent
opt-out; change the constructor to make consent opt-in by either (a) setting the
default to false in AiChatPage’s constructor signature (const
AiChatPage({super.key, this.sharesMedicalData = false});) or (b) require the
flag explicitly (const AiChatPage({super.key, required
this.sharesMedicalData});) and update all call sites that construct AiChatPage
to pass the appropriate value; ensure the final field final bool
sharesMedicalData remains unchanged.
| Future<void> _loadPatientProfile() async { | ||
| try { | ||
| final userId = Supabase.instance.client.auth.currentUser?.id; | ||
| if (userId != null) { | ||
| final profile = await Supabase.instance.client | ||
| .from('patient_profiles') | ||
| .select('maya_data_consent') | ||
| .eq('user_id', userId) | ||
| .maybeSingle(); | ||
| if (mounted) { | ||
| setState(() => patientProfile = profile); | ||
| } | ||
| } | ||
| } catch (e) { | ||
| // silent fail, will show consent page | ||
| } | ||
| } | ||
|
|
||
| void _openMayaChat() { | ||
| // check if user has already given consent | ||
| final consent = patientProfile?['maya_data_consent']; | ||
|
|
||
| if (consent == null) { | ||
| // first time - show consent page | ||
| Navigator.push(context, MaterialPageRoute(builder: (_) => const MayaConsentPage())); | ||
| } else { | ||
| // already consented - go straight to chat | ||
| Navigator.push( | ||
| context, | ||
| MaterialPageRoute(builder: (_) => AiChatPage(sharesMedicalData: consent == true)), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Don't use null for both "unknown" and "no consent".
patientProfile starts as null, is loaded asynchronously, and is never refreshed after the consent flow. That means Line 84 can route users back to MayaConsentPage even when they already made a choice—either because the fetch has not finished yet, or because they returned to HomePage after consenting/declining in the same session. Please track a separate loaded/error state and refresh patientProfile after the navigation completes, or have MayaConsentPage return the chosen value so HomePage updates immediately.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/pages/homepage.dart` around lines 62 - 94, patientProfile is
used for both "not-yet-loaded" and "no-consent", causing incorrect routing; add
a boolean state like isProfileLoaded (managed in _loadPatientProfile around the
Supabase call) and treat null patientProfile as "unknown" until isProfileLoaded
is true, and update _openMayaChat to await Navigator.push(...) when showing
MayaConsentPage so it returns the user's choice (true/false) which you then use
to setState patientProfile['maya_data_consent'] or call _loadPatientProfile() to
refresh; also ensure when navigating to AiChatPage you pass sharesMedicalData:
consent == true only after confirming consent is non-null and isProfileLoaded is
true.
| // privacy assurance | ||
| Container( | ||
| padding: const EdgeInsets.all(16), | ||
| decoration: BoxDecoration( | ||
| color: Colors.green.withOpacity(0.05), | ||
| borderRadius: BorderRadius.circular(12), | ||
| ), | ||
| child: Row( | ||
| children: [ | ||
| Icon(Icons.lock_outline, color: colorScheme.primary, size: 20), | ||
| const SizedBox(width: 12), | ||
| Expanded( | ||
| child: Text( | ||
| 'Your data stays private & encrypted. You can change this anytime in settings.', | ||
| style: TextStyle( | ||
| fontSize: 13, | ||
| color: Colors.grey[700], | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| ), |
There was a problem hiding this comment.
This privacy copy overpromises what the app currently does.
The message says the data "stays private & encrypted" and can be changed "anytime in settings", but opting in leads to profile-derived context being sent to the external chat provider from flutter_app/lib/services/chat_service.dart Line 153-Line 166, and flutter_app/lib/pages/homepage.dart Line 186-Line 190 does not expose a settings flow to change this yet. Please reword this so users are explicitly consenting to third-party processing and only promise controls that already exist.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/pages/maya_consent_page.dart` around lines 136 - 158, The
privacy blurb in MayaConsentPage currently overpromises—update the Text inside
the Container (the string that starts with 'Your data stays private &
encrypted...') to explicitly state that opting in will share profile-derived
context with a third‑party chat provider (referencing ChatService where
profile/context is sent) and remove the blanket "private & encrypted" and
"change anytime in settings" claims since homepage settings to revoke this are
not implemented; change the copy to a clear consent statement (e.g., mention
third‑party processing and how to revoke when settings are available) by editing
the Text in the Container in MayaConsentPage and ensure any matching hardcoded
strings are updated accordingly.
| if (response.statusCode == 200) { | ||
| final data = jsonDecode(response.body); | ||
| final insight = data['choices'][0]['message']['content'] as String; | ||
| if (mounted) { | ||
| setState(() { | ||
| _mayaInsight = insight.trim(); | ||
| _loadingMaya = false; | ||
| }); | ||
| } | ||
| } else { | ||
| throw Exception('API error'); | ||
| } |
There was a problem hiding this comment.
Unsafe JSON response parsing may cause runtime crash.
The response parsing at line 236 assumes a specific JSON structure without validation. If the API returns an error response, an empty choices array, or a different format, this will throw a NoSuchMethodError or TypeError.
🛡️ Proposed fix with defensive parsing
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
- final insight = data['choices'][0]['message']['content'] as String;
+ final choices = data['choices'] as List?;
+ if (choices == null || choices.isEmpty) {
+ throw Exception('No choices in response');
+ }
+ final message = choices[0]['message'] as Map<String, dynamic>?;
+ final content = message?['content'] as String?;
+ if (content == null || content.isEmpty) {
+ throw Exception('Empty response content');
+ }
+ final insight = content;
if (mounted) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/pages/mood_tracker.dart` around lines 234 - 245, The current
parsing of jsonDecode(response.body) assumes
data['choices'][0]['message']['content'] exists and is a String which can crash;
wrap the parse in a try/catch and defensively validate the structure: ensure the
decoded value is a Map, that data['choices'] is a List with at least one
element, that the first element is a Map with 'message' Map containing a String
'content'. If validation fails, set _mayaInsight to a safe fallback message (or
extracted error text from the response) and set _loadingMaya = false inside
setState (checking mounted), and for non-200 responses include response.body in
the thrown Exception or logged error so failures are observable; use the
existing symbols (response, jsonDecode, _mayaInsight, _loadingMaya, mounted,
setState) when implementing these checks.
| String get formattedDate { | ||
| final now = DateTime.now(); | ||
| final diff = now.difference(publishedAt); | ||
|
|
||
| if (diff.inHours < 1) { | ||
| return '${diff.inMinutes}m ago'; | ||
| } else if (diff.inHours < 24) { | ||
| return '${diff.inHours}h ago'; | ||
| } else if (diff.inDays < 7) { | ||
| return '${diff.inDays}d ago'; | ||
| } else { | ||
| return '${publishedAt.day}/${publishedAt.month}/${publishedAt.year}'; | ||
| } | ||
| } |
There was a problem hiding this comment.
Handle edge case of future publishedAt dates.
If publishedAt is in the future (due to timezone issues or API data), diff.inMinutes and diff.inHours will be negative, resulting in strings like "-5m ago".
Proposed fix
String get formattedDate {
final now = DateTime.now();
final diff = now.difference(publishedAt);
-
- if (diff.inHours < 1) {
+
+ if (diff.isNegative) {
+ return 'Just now';
+ } else if (diff.inHours < 1) {
return '${diff.inMinutes}m ago';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| String get formattedDate { | |
| final now = DateTime.now(); | |
| final diff = now.difference(publishedAt); | |
| if (diff.inHours < 1) { | |
| return '${diff.inMinutes}m ago'; | |
| } else if (diff.inHours < 24) { | |
| return '${diff.inHours}h ago'; | |
| } else if (diff.inDays < 7) { | |
| return '${diff.inDays}d ago'; | |
| } else { | |
| return '${publishedAt.day}/${publishedAt.month}/${publishedAt.year}'; | |
| } | |
| } | |
| String get formattedDate { | |
| final now = DateTime.now(); | |
| final diff = now.difference(publishedAt); | |
| if (diff.isNegative) { | |
| return 'Just now'; | |
| } else if (diff.inHours < 1) { | |
| return '${diff.inMinutes}m ago'; | |
| } else if (diff.inHours < 24) { | |
| return '${diff.inHours}h ago'; | |
| } else if (diff.inDays < 7) { | |
| return '${diff.inDays}d ago'; | |
| } else { | |
| return '${publishedAt.day}/${publishedAt.month}/${publishedAt.year}'; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/services/article_service.dart` around lines 70 - 83, The
formattedDate getter produces negative intervals when publishedAt is in the
future; update formattedDate to detect if publishedAt.isAfter(now) and handle
that case (e.g., return 'Just now' or a future-aware string) instead of using
negative diff values. Locate the formattedDate getter and add a guard that
checks publishedAt against DateTime.now(), then return a safe value or clamp the
Duration to zero before computing '${diff.inMinutes}m ago', '${diff.inHours}h
ago', etc., ensuring no negative numbers are shown.
| 'apiKey': _apiKey, | ||
| }); | ||
|
|
||
| final response = await http.get(uri); |
There was a problem hiding this comment.
Missing HTTP request timeout.
The http.get calls have no timeout, which could cause the app to hang indefinitely on slow or unresponsive networks. Consider using the timeout parameter or wrapping with a timeout.
Proposed fix
- final response = await http.get(uri);
+ final response = await http.get(uri).timeout(
+ const Duration(seconds: 15),
+ onTimeout: () => throw Exception('Request timed out'),
+ );Apply to both fetchArticles (line 121) and fetchPopularArticles (line 166).
Also applies to: 166-166
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/services/article_service.dart` at line 121, Add a finite
timeout to the HTTP requests in fetchArticles and fetchPopularArticles by
replacing the raw http.get(uri) calls with a timed request (e.g.,
http.get(uri).timeout(Duration(seconds: X))) and handle TimeoutException (import
dart:async) to return a controlled error/response instead of hanging; ensure
both functions reference their existing http.get calls and catch/handle
TimeoutException consistently (logging or throwing a descriptive exception) so
callers receive a predictable failure on network timeouts.
| ChatService({bool usePersonalData = true}) : _usePersonalData = usePersonalData { | ||
| _initializeWithProfile(); | ||
| } |
There was a problem hiding this comment.
Opt-out still loads the user's medical profile.
usePersonalData: false only changes Line 94's prompt selection; the constructor still calls _initializeWithProfile(), which fetches the full patient_profiles row first. So opted-out users still have sensitive data pulled into memory, and refreshProfile() will keep doing the same. Short-circuit the initialization when _usePersonalData is false so no profile data is read at all.
Proposed fix
ChatService({bool usePersonalData = true}) : _usePersonalData = usePersonalData {
- _initializeWithProfile();
+ if (_usePersonalData) {
+ _initializeWithProfile();
+ } else {
+ _conversationHistory.add(ChatMessage(role: 'system', content: _basePrompt));
+ _profileLoaded = true;
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ChatService({bool usePersonalData = true}) : _usePersonalData = usePersonalData { | |
| _initializeWithProfile(); | |
| } | |
| ChatService({bool usePersonalData = true}) : _usePersonalData = usePersonalData { | |
| if (_usePersonalData) { | |
| _initializeWithProfile(); | |
| } else { | |
| _conversationHistory.add(ChatMessage(role: 'system', content: _basePrompt)); | |
| _profileLoaded = true; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/services/chat_service.dart` around lines 64 - 66, The
constructor currently always calls _initializeWithProfile(), causing
patient_profiles to be loaded even when _usePersonalData is false; modify the
ChatService constructor (or add a guard at the start of
_initializeWithProfile()) to short-circuit and return immediately when
_usePersonalData is false, and ensure refreshProfile() also checks
_usePersonalData before fetching the patient_profiles row so no sensitive
profile data is read into memory for opted-out users.
What's Changed
Mood Tracker
Articles
New Files
lib/services/article_service.dartlib/pages/article_reader_page.dartDependencies
share_plusfor article sharingWill focus on admin side of things now & a few bug fixes
Summary by CodeRabbit
Release Notes
New Features
Improvements