Skip to content

[FOSSOVERFLOW-26] per-user mood tracking, Maya insights & privacy layer, News API articles#9

Open
Ashish-Kumar-Dash wants to merge 3 commits intoOpenLake:mainfrom
Ashish-Kumar-Dash:akd
Open

[FOSSOVERFLOW-26] per-user mood tracking, Maya insights & privacy layer, News API articles#9
Ashish-Kumar-Dash wants to merge 3 commits intoOpenLake:mainfrom
Ashish-Kumar-Dash:akd

Conversation

@Ashish-Kumar-Dash
Copy link
Contributor

@Ashish-Kumar-Dash Ashish-Kumar-Dash commented Mar 10, 2026

What's Changed

Mood Tracker

  • Migrated mood entries from SharedPreferences to Supabase (per-user storage)
  • Added "Maya's Insights" card - AI analyzes mood patterns and gives personalized advice
  • Added a privacy layer to Maya to protect medical info of users being shared with shared

Articles

  • Integrated News API for real wellness articles
  • In-app article reader (no browser redirect)
  • Category filtering (Meditation, Anxiety, Stress, Sleep, Self Growth)
  • Popular articles carousel
  • HTML tag stripping for clean content display

New Files

  • lib/services/article_service.dart
  • lib/pages/article_reader_page.dart

Dependencies

  • Added share_plus for article sharing

Will focus on admin side of things now & a few bug fixes

Summary by CodeRabbit

Release Notes

  • New Features

    • Added health data sharing consent page for personalized Maya interactions
    • Introduced in-app article reader with sharing and browser capabilities
    • Articles page redesign with dynamic content, category filtering, and pull-to-refresh
    • Mood tracker now generates personalized AI insights from mood history
  • Improvements

    • Mood tracking data now syncs to cloud backend
    • Chat service respects user privacy preferences for medical data usage
    • Enhanced article card design with source and publication time metadata

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Walkthrough

This 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

Cohort / File(s) Summary
Consent & Chat Flow
lib/pages/maya_consent_page.dart, lib/pages/ai_chat.dart, lib/pages/homepage.dart, lib/services/chat_service.dart
Introduces new MayaConsentPage for medical data sharing consent. Updates AiChatPage with sharesMedicalData flag and defers ChatService initialization to initState. Modifies ChatService constructor to accept usePersonalData parameter that controls system prompt selection. Updates homepage.dart to load patient profile and route through consent flow before AI chat access.
Article System
lib/services/article_service.dart, lib/pages/article_reader_page.dart, lib/pages/article_page.dart, lib/widgets/article_card.dart
Introduces new ArticleService for fetching articles from News API with category/popularity filtering. Adds ArticleReaderPage for rendering full article content with sharing and browser launch actions. Completely redesigns ArticlePage with dynamic article loading, caching, error/loading states, pull-to-refresh, and category chips. Extends ArticleCard with network/asset image support, metadata display, and refreshed layout.
Data Persistence
lib/pages/mood_tracker.dart, lib/pages/profile_page.dart
Migrates mood tracking from local SharedPreferences to Supabase backend with per-user scoping. Adds MoodEntry Supabase serialization methods and integrates Maya insights feature via OpenRouter API. Changes profile_page.dart from upsert to filtered update operation on patient_profiles table.
Dependencies & Platform Support
pubspec.yaml, macos/Flutter/GeneratedPluginRegistrant.swift, windows/flutter/generated_plugin_registrant.cc, windows/flutter/generated_plugins.cmake
Adds share_plus dependency (v10.0.0) and registers SharePlus plugin for macOS and Windows platforms.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hops with glee through code so bright,
Consent flows guard the user's right,
Articles fetch from far and wide,
Maya whispers wisdom as our guide,
Mood and health dance side by side! ✨📰💭

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: per-user mood tracking, Maya insights with privacy layer, and News API articles integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🔴 Critical

Critical: Orphaned code outside class scope will cause compilation error.

Lines 1277-1505 contain a build method with @override annotation 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 build method 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.post call 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:convert doesn't have HTML decoding, but the html_unescape package or html package 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 deprecated withOpacity with withValues.

withOpacity() is deprecated in Flutter due to precision loss when handling floating-point alpha values; use withValues(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: imagePath field is currently unused.

Per context snippet 1, the only caller (article_page.dart) always passes imageUrl and never imagePath. 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 deprecated withOpacity with withValues(alpha: …).

Lines 33, 61, and 78 use the deprecated Color.withOpacity() method (deprecated since Flutter 3.27). Replace with withValues(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 canLaunchUrl returns false, 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 context from 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 deprecated withOpacity with withValues.

Color.withOpacity is deprecated in Flutter 3.27+. Replace with Color.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

📥 Commits

Reviewing files that changed from the base of the PR and between 4cf83f0 and 9de6715.

⛔ Files ignored due to path filters (1)
  • flutter_app/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • flutter_app/lib/pages/ai_chat.dart
  • flutter_app/lib/pages/article_page.dart
  • flutter_app/lib/pages/article_reader_page.dart
  • flutter_app/lib/pages/homepage.dart
  • flutter_app/lib/pages/maya_consent_page.dart
  • flutter_app/lib/pages/mood_tracker.dart
  • flutter_app/lib/pages/profile_page.dart
  • flutter_app/lib/services/article_service.dart
  • flutter_app/lib/services/chat_service.dart
  • flutter_app/lib/widgets/article_card.dart
  • flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift
  • flutter_app/pubspec.yaml
  • flutter_app/windows/flutter/generated_plugin_registrant.cc
  • flutter_app/windows/flutter/generated_plugins.cmake

Comment on lines +6 to +8
final bool sharesMedicalData;

const AiChatPage({super.key, this.sharesMedicalData = true});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +62 to +94
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)),
);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +136 to +158
// 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],
),
),
),
],
),
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +234 to +245
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');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +70 to +83
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}';
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +64 to 66
ChatService({bool usePersonalData = true}) : _usePersonalData = usePersonalData {
_initializeWithProfile();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

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.

1 participant