Add AI chatbot with OpenRouter and patient onboarding flow#5
Add AI chatbot with OpenRouter and patient onboarding flow#5Ashish-Kumar-Dash wants to merge 3 commits intoOpenLake:mainfrom
Conversation
WalkthroughThis PR introduces an onboarding flow, environment variable management, user profile management, and a ChatService-backed AI chat system. New pages handle patient onboarding and profile editing, auth flow validates onboarding status, and the chat UI now leverages a service layer that integrates with OpenRouter for AI responses personalized with patient data. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant AuthWrapper
participant Supabase
participant OnboardingPage
participant HomePage
User->>AuthWrapper: Login
AuthWrapper->>AuthWrapper: Check onboarding status
AuthWrapper->>Supabase: Query patient_profiles
alt Onboarding Incomplete
AuthWrapper->>OnboardingPage: Display
User->>OnboardingPage: Fill form & submit
OnboardingPage->>Supabase: Upsert patient_profiles
OnboardingPage->>AuthWrapper: onComplete callback
AuthWrapper->>HomePage: Navigate
else Onboarding Complete
AuthWrapper->>HomePage: Navigate directly
end
sequenceDiagram
participant User
participant ChatUI as AI Chat Page
participant ChatService
participant Supabase
participant OpenRouter
User->>ChatUI: Send message
ChatUI->>ChatService: sendMessage(text)
ChatService->>Supabase: Load patient profile (if needed)
ChatService->>ChatService: Build personalized system prompt
ChatService->>OpenRouter: POST request with messages & API key
OpenRouter-->>ChatService: AI response
ChatService->>ChatService: Append to history
ChatService-->>ChatUI: Return response
ChatUI->>ChatUI: Display message & scroll
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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: 8
🧹 Nitpick comments (2)
flutter_app/lib/services/chat_service.dart (1)
121-125: Busy-wait loop for profile loading is inefficient.The
while (!_profileLoaded)loop withFuture.delayedis a polling anti-pattern that wastes CPU cycles. Use aCompleterto await initialization properly.♻️ Suggested refactor
+import 'dart:async'; + class ChatService { // ... + final Completer<void> _initCompleter = Completer<void>(); ChatService() { _initializeWithProfile(); } Future<void> _initializeWithProfile() async { await _loadPatientProfile(); final systemPrompt = _buildSystemPrompt(); _conversationHistory.add(ChatMessage(role: 'system', content: systemPrompt)); _profileLoaded = true; + _initCompleter.complete(); } Future<String> sendMessage(String userMessage) async { - // wait for profile to load if not ready - while (!_profileLoaded) { - await Future.delayed(const Duration(milliseconds: 100)); - } + // wait for initialization to complete + await _initCompleter.future;🤖 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 121 - 125, Replace the inefficient polling in sendMessage (while (!_profileLoaded) await Future.delayed(...)) with a Completer-based wait: add a Completer<void> (e.g., _profileLoadedCompleter) that is completed when the profile finishes loading, update the profile initialization logic to call _profileLoaded = true and _profileLoadedCompleter.complete() (or complete only if not completed), and in sendMessage await _profileLoadedCompleter.future instead of looping; ensure the completer is created/reset appropriately (e.g., new Completer() at start or when reloading) so sendMessage reliably awaits initialization without busy-waiting.flutter_app/lib/main.dart (1)
15-19: Hardcoded Supabase credentials in source code.The Supabase URL and
anonKeyare hardcoded whiledotenvis loaded but not used here. This is inconsistent with howchat_service.dartcorrectly readsOPENROUTER_API_KEYfromdotenv.env[].While the Supabase
anonKeyis designed to be public (protected by Row Level Security), committing it to source control:
- Makes key rotation difficult
- Creates inconsistency in configuration management
Consider reading these from environment variables for consistency:
🔧 Suggested refactor
// Initialize Supabase await Supabase.initialize( - url: 'https://prvbbbnsizxxfxreokov.supabase.co', - anonKey: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBydmJiYm5zaXp4eGZ4cmVva292Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA4OTMwODgsImV4cCI6MjA4NjQ2OTA4OH0.GOqP5k0AnGzqse5loXuPz9BkfGCUdJgApUL9QBVw0es', + url: dotenv.env['SUPABASE_URL'] ?? '', + anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '', );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@flutter_app/lib/main.dart` around lines 15 - 19, Supabase.initialize is using hardcoded URL and anonKey; change it to read values from environment (e.g. dotenv.env['SUPABASE_URL'] and dotenv.env['SUPABASE_ANON_KEY'] or from Dart defines) and fail fast if missing; update the call in main (the Supabase.initialize invocation) to pass the env values instead of literals, ensure dotenv is loaded before this call (or use Platform.environment/const String.fromEnvironment fallback) and add a clear error/log if either env var is absent to avoid silent misconfiguration.
🤖 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/auth/auth_wrapper.dart`:
- Around line 91-94: The code mutates _onboardingComplete directly inside
build(); move this state change out of build and update it via setState (or in a
proper lifecycle/stream callback). For example, when the auth
stream/StreamBuilder indicates a logged-out user, call if (mounted) setState(()
=> _onboardingComplete = null) from the stream callback or from
didUpdateWidget/didChangeDependencies instead of assigning _onboardingComplete =
null inside build() so state changes trigger proper rebuilds; locate references
to _onboardingComplete, build(), and the StreamBuilder in AuthWrapper to apply
the fix.
In `@flutter_app/lib/pages/ai_chat.dart`:
- Around line 102-111: The "Start over" handler can clear the UI while a prior
request is still in flight and allow a stale reply to reappear; update the
onPressed logic for the Start over button (or the widget that supplies
onPressed) to first check the component state flag _isLoading and do nothing if
true (or disable the button by supplying null when _isLoading is true).
Specifically, guard the existing calls to _chatService.clearHistory() and
_messages.clear()/add(...) behind a check for !_isLoading, or disable the Start
over control when _isLoading is true, so you never reset state while a request
is pending.
- Around line 55-79: The _sendMessage flow can hang and exposes raw exceptions;
wrap the call to _chatService.sendMessage in a timeout (e.g., using .timeout
with a reasonable Duration) and handle TimeoutException separately, ensure any
catch/finally always resets _isLoading via setState when mounted so the UI never
stays loading; replace the SnackBar content to show a user-friendly message like
"Service unavailable, please try again" while logging the raw exception (or
error) internally, and add a fallback message into _messages (e.g., "Oops,
something went wrong. Please try again.") instead of the raw exception text;
adjust references: _sendMessage, _chatService.sendMessage, setState, _isLoading,
_messages, ScaffoldMessenger.of(context).showSnackBar.
In `@flutter_app/lib/pages/onboarding_page.dart`:
- Around line 84-86: The display_id is derived via user.id.substring(0,
8).toUpperCase() in the upsert to patient_profiles and can collide at scale;
update the implementation to either (A) generate a truly unique user-facing ID
(e.g., sequential generator or UUIDv5-based function) and write that value to
display_id instead of the truncated UUID, or (B) add a uniqueness check before
inserting (query patient_profiles for existing display_id and regenerate until
unique) and fail or retry on collision; if you choose not to change behavior,
add a clear comment/docstring next to the upsert explaining that display_id is
only a convenience, not guaranteed unique.
In `@flutter_app/lib/pages/profile_page.dart`:
- Around line 96-142: The _saveProfile method currently writes to Supabase
without validating inputs; add pre-save validation in _saveProfile to check that
_nameController.text.trim() is not empty and that
int.tryParse(_ageController.text) yields an age between 13 and 120 (inclusive);
if validation fails, set _isSaving=false (if set), show a red SnackBar with a
clear message (e.g., "Please enter a valid name" or "Age must be between 13 and
120") and return early without calling
Supabase.instance.client.from('patient_profiles').upsert; keep existing mounted
checks and state updates (_isEditing/_isSaving) and ensure values used in the
upsert (display_id, age, gender) use the validated/parsed variables.
In `@flutter_app/lib/services/chat_service.dart`:
- Around line 151-161: The code reads data['choices'][0]['message']['content']
without validating structure which can throw if choices is missing/empty; update
the success-path in the response.statusCode == 200 branch to defensively parse:
check that data is a Map, that data['choices'] is a non-empty List, and that
choices[0]['message']['content'] is a String (or provide a safe fallback like
'No response'); only then create aiReply, add ChatMessage(role: 'assistant',
content: aiReply) to _conversationHistory, otherwise log the unexpected payload
and throw a clear Exception indicating malformed API response so callers don’t
get silent runtime exceptions.
- Around line 134-149: The HTTP POST to Uri.parse(_baseUrl) that uses _apiKey,
_model and _conversationHistory should include a timeout to avoid hanging;
modify the await http.post(...) call to use .timeout(Duration(seconds: 15)) (or
a configurable timeout constant) and add handling for a TimeoutException in the
surrounding try/catch so you can log/return a friendly timeout error instead of
waiting forever; ensure you import dart:async for TimeoutException and adapt the
existing catch block to distinguish timeout failures from other exceptions.
In `@flutter_app/pubspec.yaml`:
- Line 70: The .env file is currently listed as a Flutter asset in pubspec.yaml
(the ".env" entry) which bundles secrets like OPENROUTER_API_KEY into the app;
remove the ".env" entry from the assets list in pubspec.yaml so it is not
included in the compiled APK/IPA, add .env to .gitignore to avoid committing it,
and migrate secret usage to a secure pattern (e.g., proxy requests through your
backend or use OAuth) or—if you accept a public client-side key—document that
decision and switch to safer build-time injection (e.g., dart-define) instead of
bundling .env as an asset.
---
Nitpick comments:
In `@flutter_app/lib/main.dart`:
- Around line 15-19: Supabase.initialize is using hardcoded URL and anonKey;
change it to read values from environment (e.g. dotenv.env['SUPABASE_URL'] and
dotenv.env['SUPABASE_ANON_KEY'] or from Dart defines) and fail fast if missing;
update the call in main (the Supabase.initialize invocation) to pass the env
values instead of literals, ensure dotenv is loaded before this call (or use
Platform.environment/const String.fromEnvironment fallback) and add a clear
error/log if either env var is absent to avoid silent misconfiguration.
In `@flutter_app/lib/services/chat_service.dart`:
- Around line 121-125: Replace the inefficient polling in sendMessage (while
(!_profileLoaded) await Future.delayed(...)) with a Completer-based wait: add a
Completer<void> (e.g., _profileLoadedCompleter) that is completed when the
profile finishes loading, update the profile initialization logic to call
_profileLoaded = true and _profileLoadedCompleter.complete() (or complete only
if not completed), and in sendMessage await _profileLoadedCompleter.future
instead of looping; ensure the completer is created/reset appropriately (e.g.,
new Completer() at start or when reloading) so sendMessage reliably awaits
initialization without busy-waiting.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8058634b-8c22-444f-b142-a0110d811e99
⛔ Files ignored due to path filters (1)
flutter_app/pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
flutter_app/.gitignoreflutter_app/lib/auth/auth_wrapper.dartflutter_app/lib/main.dartflutter_app/lib/pages/ai_chat.dartflutter_app/lib/pages/homepage.dartflutter_app/lib/pages/onboarding_page.dartflutter_app/lib/pages/profile_page.dartflutter_app/lib/services/chat_service.dartflutter_app/pubspec.yaml
| // reset onboarding check when logged out | ||
| if (_onboardingComplete != null) { | ||
| _onboardingComplete = null; | ||
| } |
There was a problem hiding this comment.
State mutation during build() without setState.
Directly setting _onboardingComplete = null inside build() mutates state without calling setState. While it may work because the StreamBuilder triggers rebuilds, this is an anti-pattern that can lead to inconsistent behavior.
🔧 Suggested fix
// reset onboarding check when logged out
if (_onboardingComplete != null) {
- _onboardingComplete = null;
+ // Schedule state reset for after build completes
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ setState(() {
+ _onboardingComplete = null;
+ _checkingOnboarding = false;
+ });
+ }
+ });
}📝 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.
| // reset onboarding check when logged out | |
| if (_onboardingComplete != null) { | |
| _onboardingComplete = null; | |
| } | |
| // reset onboarding check when logged out | |
| if (_onboardingComplete != null) { | |
| // Schedule state reset for after build completes | |
| WidgetsBinding.instance.addPostFrameCallback((_) { | |
| if (mounted) { | |
| setState(() { | |
| _onboardingComplete = null; | |
| _checkingOnboarding = false; | |
| }); | |
| } | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/auth/auth_wrapper.dart` around lines 91 - 94, The code
mutates _onboardingComplete directly inside build(); move this state change out
of build and update it via setState (or in a proper lifecycle/stream callback).
For example, when the auth stream/StreamBuilder indicates a logged-out user,
call if (mounted) setState(() => _onboardingComplete = null) from the stream
callback or from didUpdateWidget/didChangeDependencies instead of assigning
_onboardingComplete = null inside build() so state changes trigger proper
rebuilds; locate references to _onboardingComplete, build(), and the
StreamBuilder in AuthWrapper to apply the fix.
| try { | ||
| final response = await _chatService.sendMessage(text); | ||
|
|
||
| if (mounted) { | ||
| setState(() { | ||
| _messages.add({"text": response, "isUser": false}); | ||
| _isLoading = false; | ||
| }); | ||
| }); | ||
| }); | ||
| _scrollToBottom(); | ||
| } | ||
| } catch (e) { | ||
| // handle errors gracefully | ||
| if (mounted) { | ||
| setState(() { | ||
| _messages.add({ | ||
| "text": "Oops, something went wrong. Check your connection?", | ||
| "isUser": false, | ||
| }); | ||
| _isLoading = false; | ||
| }); | ||
|
|
||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Harden _sendMessage against indefinite waits and raw error exposure.
sendMessage can block while profile initialization never completes, which leaves _isLoading stuck. Also, exposing raw exception text in SnackBar is not ideal for user-facing UX.
Suggested patch
+import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_app/widgets/chat_bubble.dart';
import 'package:flutter_app/services/chat_service.dart';
@@
try {
- final response = await _chatService.sendMessage(text);
+ final response = await _chatService
+ .sendMessage(text)
+ .timeout(const Duration(seconds: 30));
@@
- } catch (e) {
+ } on TimeoutException {
+ if (mounted) {
+ setState(() {
+ _messages.add({
+ "text": "I'm taking too long to respond. Please try again.",
+ "isUser": false,
+ });
+ _isLoading = false;
+ });
+ _scrollToBottom();
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Request timed out. Please try again.'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ } catch (_) {
// handle errors gracefully
if (mounted) {
setState(() {
_messages.add({
"text": "Oops, something went wrong. Check your connection?",
"isUser": false,
});
_isLoading = false;
});
+ _scrollToBottom();
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
+ const SnackBar(
+ content: Text('Could not send message. Please try again.'),
+ backgroundColor: Colors.red,
+ ),
);
}
}🤖 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 55 - 79, The _sendMessage
flow can hang and exposes raw exceptions; wrap the call to
_chatService.sendMessage in a timeout (e.g., using .timeout with a reasonable
Duration) and handle TimeoutException separately, ensure any catch/finally
always resets _isLoading via setState when mounted so the UI never stays
loading; replace the SnackBar content to show a user-friendly message like
"Service unavailable, please try again" while logging the raw exception (or
error) internally, and add a fallback message into _messages (e.g., "Oops,
something went wrong. Please try again.") instead of the raw exception text;
adjust references: _sendMessage, _chatService.sendMessage, setState, _isLoading,
_messages, ScaffoldMessenger.of(context).showSnackBar.
| onPressed: () { | ||
| setState(() { | ||
| _chatService.clearHistory(); | ||
| _messages.clear(); | ||
| _messages.add({ | ||
| "text": "Hey there! How are you feeling today? I'm here to chat. 💚", | ||
| "isUser": false, | ||
| }); | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Disable “Start over” while a request is in flight.
Resetting during _isLoading can clear the visible chat and then append a stale reply from the previous request.
Suggested patch
- onPressed: () {
+ onPressed: _isLoading ? null : () {
setState(() {
_chatService.clearHistory();
_messages.clear();
_messages.add({
"text": "Hey there! How are you feeling today? I'm here to chat. 💚",
"isUser": false,
});
});
},📝 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.
| onPressed: () { | |
| setState(() { | |
| _chatService.clearHistory(); | |
| _messages.clear(); | |
| _messages.add({ | |
| "text": "Hey there! How are you feeling today? I'm here to chat. 💚", | |
| "isUser": false, | |
| }); | |
| }); | |
| }, | |
| onPressed: _isLoading ? null : () { | |
| setState(() { | |
| _chatService.clearHistory(); | |
| _messages.clear(); | |
| _messages.add({ | |
| "text": "Hey there! How are you feeling today? I'm here to chat. 💚", | |
| "isUser": false, | |
| }); | |
| }); | |
| }, |
🤖 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 102 - 111, The "Start over"
handler can clear the UI while a prior request is still in flight and allow a
stale reply to reappear; update the onPressed logic for the Start over button
(or the widget that supplies onPressed) to first check the component state flag
_isLoading and do nothing if true (or disable the button by supplying null when
_isLoading is true). Specifically, guard the existing calls to
_chatService.clearHistory() and _messages.clear()/add(...) behind a check for
!_isLoading, or disable the Start over control when _isLoading is true, so you
never reset state while a request is pending.
| await Supabase.instance.client.from('patient_profiles').upsert({ | ||
| 'user_id': user.id, | ||
| 'display_id': user.id.substring(0, 8).toUpperCase(), // short readable ID |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for SQL schema files and display_id constraints
find . -type f -name "*.sql" -o -name "*schema*" -o -name "*migration*" | head -20Repository: OpenLake/GetWelPlus
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Search for display_id references throughout the codebase
rg 'display_id' -n --type sql --type dartRepository: OpenLake/GetWelPlus
Length of output: 570
🏁 Script executed:
#!/bin/bash
# Check if there are database migration files
find . -type d -name migrations -o -name database -o -name db | head -10Repository: OpenLake/GetWelPlus
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Look for Supabase configuration and schema definitions
find . -type f \( -name "*.json" -o -name "*.yaml" -o -name "*.yml" \) | xargs grep -l "supabase\|display_id" 2>/dev/null | head -10Repository: OpenLake/GetWelPlus
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Check the full context around the upsert call in onboarding_page.dart
sed -n '75,95p' flutter_app/lib/pages/onboarding_page.dartRepository: OpenLake/GetWelPlus
Length of output: 1030
🏁 Script executed:
#!/bin/bash
# Check if there's error handling around the upsert operation
sed -n '70,110p' flutter_app/lib/pages/onboarding_page.dartRepository: OpenLake/GetWelPlus
Length of output: 1573
🏁 Script executed:
#!/bin/bash
# Look for any README or documentation about schema
find . -type f -name "README*" -o -name "*.md" | xargs grep -l "display_id\|schema" 2>/dev/null | head -5Repository: OpenLake/GetWelPlus
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Look for any documentation about display_id or database schema
find . -type f \( -name "*.md" -o -name "*.txt" \) -exec grep -l "display_id\|patient_profiles\|schema" {} \;Repository: OpenLake/GetWelPlus
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Check if there's a Supabase migration or schema file
find . -type f -name "*supabase*" -o -path "*supabase*" 2>/dev/null | head -20Repository: OpenLake/GetWelPlus
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Check if display_id is used as a lookup/unique reference anywhere
rg 'display_id' -B 2 -A 2 --type dartRepository: OpenLake/GetWelPlus
Length of output: 2012
display_id collision risk has low practical impact but should be documented.
Using user.id.substring(0, 8).toUpperCase() truncates the UUID to 8 hex characters (~4 billion combinations). While collisions become probable at scale (birthday paradox: ~0.1% at ~65k users), the current implementation is safe from data corruption since the upsert is keyed on user_id (the full UUID).
However, if display_id is intended as a user-facing identifier for communication or sharing between users, collisions could cause confusion. If this is the case, consider either:
- Using a dedicated sequential or UUIDv5-based ID generator
- Adding validation to ensure uniqueness before insertion
- Documenting that
display_idis a convenience field with no uniqueness guarantee
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/pages/onboarding_page.dart` around lines 84 - 86, The
display_id is derived via user.id.substring(0, 8).toUpperCase() in the upsert to
patient_profiles and can collide at scale; update the implementation to either
(A) generate a truly unique user-facing ID (e.g., sequential generator or
UUIDv5-based function) and write that value to display_id instead of the
truncated UUID, or (B) add a uniqueness check before inserting (query
patient_profiles for existing display_id and regenerate until unique) and fail
or retry on collision; if you choose not to change behavior, add a clear
comment/docstring next to the upsert explaining that display_id is only a
convenience, not guaranteed unique.
| Future<void> _saveProfile() async { | ||
| setState(() => _isSaving = true); | ||
|
|
||
| try { | ||
| final user = Supabase.instance.client.auth.currentUser; | ||
| if (user == null) throw Exception('Not logged in'); | ||
|
|
||
| await Supabase.instance.client.from('patient_profiles').upsert({ | ||
| 'user_id': user.id, | ||
| 'display_id': _profile?['display_id'] ?? user.id.substring(0, 8).toUpperCase(), | ||
| 'full_name': _nameController.text.trim(), | ||
| 'age': int.tryParse(_ageController.text) ?? 0, | ||
| 'gender': _selectedGender, | ||
| 'phone': _phoneController.text.trim(), | ||
| 'emergency_contact_name': _emergencyNameController.text.trim(), | ||
| 'emergency_contact_phone': _emergencyPhoneController.text.trim(), | ||
| 'medical_conditions': _conditionsController.text.trim(), | ||
| 'current_medications': _medicationsController.text.trim(), | ||
| 'allergies': _allergiesController.text.trim(), | ||
| 'mental_health_concerns': _mentalHealthController.text.trim(), | ||
| 'therapy_history': _therapyHistoryController.text.trim(), | ||
| 'onboarding_complete': true, | ||
| 'updated_at': DateTime.now().toIso8601String(), | ||
| }); | ||
|
|
||
| if (mounted) { | ||
| setState(() { | ||
| _isEditing = false; | ||
| _isSaving = false; | ||
| }); | ||
| _loadProfile(); // refresh | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('Profile updated!'), | ||
| backgroundColor: Colors.green, | ||
| ), | ||
| ); | ||
| } | ||
| } catch (e) { | ||
| if (mounted) { | ||
| setState(() => _isSaving = false); | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), | ||
| ); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing input validation before save.
The _saveProfile method saves data without validating inputs, unlike OnboardingPage which validates name and age (13-120 range). A user could save an empty name or invalid age.
🛡️ Suggested validation
Future<void> _saveProfile() async {
+ // Basic validation
+ if (_nameController.text.trim().isEmpty) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Name is required'), backgroundColor: Colors.red),
+ );
+ return;
+ }
+ final age = int.tryParse(_ageController.text);
+ if (age == null || age < 13 || age > 120) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Please enter a valid age (13-120)'), backgroundColor: Colors.red),
+ );
+ return;
+ }
+
setState(() => _isSaving = 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.
| Future<void> _saveProfile() async { | |
| setState(() => _isSaving = true); | |
| try { | |
| final user = Supabase.instance.client.auth.currentUser; | |
| if (user == null) throw Exception('Not logged in'); | |
| await Supabase.instance.client.from('patient_profiles').upsert({ | |
| 'user_id': user.id, | |
| 'display_id': _profile?['display_id'] ?? user.id.substring(0, 8).toUpperCase(), | |
| 'full_name': _nameController.text.trim(), | |
| 'age': int.tryParse(_ageController.text) ?? 0, | |
| 'gender': _selectedGender, | |
| 'phone': _phoneController.text.trim(), | |
| 'emergency_contact_name': _emergencyNameController.text.trim(), | |
| 'emergency_contact_phone': _emergencyPhoneController.text.trim(), | |
| 'medical_conditions': _conditionsController.text.trim(), | |
| 'current_medications': _medicationsController.text.trim(), | |
| 'allergies': _allergiesController.text.trim(), | |
| 'mental_health_concerns': _mentalHealthController.text.trim(), | |
| 'therapy_history': _therapyHistoryController.text.trim(), | |
| 'onboarding_complete': true, | |
| 'updated_at': DateTime.now().toIso8601String(), | |
| }); | |
| if (mounted) { | |
| setState(() { | |
| _isEditing = false; | |
| _isSaving = false; | |
| }); | |
| _loadProfile(); // refresh | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar( | |
| content: Text('Profile updated!'), | |
| backgroundColor: Colors.green, | |
| ), | |
| ); | |
| } | |
| } catch (e) { | |
| if (mounted) { | |
| setState(() => _isSaving = false); | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), | |
| ); | |
| } | |
| } | |
| } | |
| Future<void> _saveProfile() async { | |
| // Basic validation | |
| if (_nameController.text.trim().isEmpty) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('Name is required'), backgroundColor: Colors.red), | |
| ); | |
| return; | |
| } | |
| final age = int.tryParse(_ageController.text); | |
| if (age == null || age < 13 || age > 120) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('Please enter a valid age (13-120)'), backgroundColor: Colors.red), | |
| ); | |
| return; | |
| } | |
| setState(() => _isSaving = true); | |
| try { | |
| final user = Supabase.instance.client.auth.currentUser; | |
| if (user == null) throw Exception('Not logged in'); | |
| await Supabase.instance.client.from('patient_profiles').upsert({ | |
| 'user_id': user.id, | |
| 'display_id': _profile?['display_id'] ?? user.id.substring(0, 8).toUpperCase(), | |
| 'full_name': _nameController.text.trim(), | |
| 'age': int.tryParse(_ageController.text) ?? 0, | |
| 'gender': _selectedGender, | |
| 'phone': _phoneController.text.trim(), | |
| 'emergency_contact_name': _emergencyNameController.text.trim(), | |
| 'emergency_contact_phone': _emergencyPhoneController.text.trim(), | |
| 'medical_conditions': _conditionsController.text.trim(), | |
| 'current_medications': _medicationsController.text.trim(), | |
| 'allergies': _allergiesController.text.trim(), | |
| 'mental_health_concerns': _mentalHealthController.text.trim(), | |
| 'therapy_history': _therapyHistoryController.text.trim(), | |
| 'onboarding_complete': true, | |
| 'updated_at': DateTime.now().toIso8601String(), | |
| }); | |
| if (mounted) { | |
| setState(() { | |
| _isEditing = false; | |
| _isSaving = false; | |
| }); | |
| _loadProfile(); // refresh | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar( | |
| content: Text('Profile updated!'), | |
| backgroundColor: Colors.green, | |
| ), | |
| ); | |
| } | |
| } catch (e) { | |
| if (mounted) { | |
| setState(() => _isSaving = false); | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), | |
| ); | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/lib/pages/profile_page.dart` around lines 96 - 142, The
_saveProfile method currently writes to Supabase without validating inputs; add
pre-save validation in _saveProfile to check that _nameController.text.trim() is
not empty and that int.tryParse(_ageController.text) yields an age between 13
and 120 (inclusive); if validation fails, set _isSaving=false (if set), show a
red SnackBar with a clear message (e.g., "Please enter a valid name" or "Age
must be between 13 and 120") and return early without calling
Supabase.instance.client.from('patient_profiles').upsert; keep existing mounted
checks and state updates (_isEditing/_isSaving) and ensure values used in the
upsert (display_id, age, gender) use the validated/parsed variables.
| try { | ||
| final response = await http.post( | ||
| Uri.parse(_baseUrl), | ||
| headers: { | ||
| 'Authorization': 'Bearer $_apiKey', | ||
| 'Content-Type': 'application/json', | ||
| 'HTTP-Referer': 'https://getwelplus.app', | ||
| 'X-Title': 'GetWel+', | ||
| }, | ||
| body: jsonEncode({ | ||
| 'model': _model, | ||
| 'messages': _conversationHistory.map((m) => m.toJson()).toList(), | ||
| 'temperature': 0.7, // bit of creativity but not too wild | ||
| 'max_tokens': 1024, | ||
| }), | ||
| ); |
There was a problem hiding this comment.
HTTP request lacks timeout.
The http.post call has no timeout, which could cause the app to hang indefinitely if the network is slow or the server is unresponsive.
🛡️ Suggested fix
- final response = await http.post(
+ final response = await http.post(
Uri.parse(_baseUrl),
headers: {
'Authorization': 'Bearer $_apiKey',
'Content-Type': 'application/json',
'HTTP-Referer': 'https://getwelplus.app',
'X-Title': 'GetWel+',
},
body: jsonEncode({
'model': _model,
'messages': _conversationHistory.map((m) => m.toJson()).toList(),
'temperature': 0.7,
'max_tokens': 1024,
}),
- );
+ ).timeout(const Duration(seconds: 30));🤖 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 134 - 149, The HTTP
POST to Uri.parse(_baseUrl) that uses _apiKey, _model and _conversationHistory
should include a timeout to avoid hanging; modify the await http.post(...) call
to use .timeout(Duration(seconds: 15)) (or a configurable timeout constant) and
add handling for a TimeoutException in the surrounding try/catch so you can
log/return a friendly timeout error instead of waiting forever; ensure you
import dart:async for TimeoutException and adapt the existing catch block to
distinguish timeout failures from other exceptions.
| if (response.statusCode == 200) { | ||
| final data = jsonDecode(response.body); | ||
| final aiReply = data['choices'][0]['message']['content'] as String; | ||
|
|
||
| // save AI's response for context | ||
| _conversationHistory.add(ChatMessage(role: 'assistant', content: aiReply)); | ||
| return aiReply; | ||
| } else { | ||
| final err = jsonDecode(response.body); | ||
| throw Exception(err['error']?['message'] ?? 'Something went wrong'); | ||
| } |
There was a problem hiding this comment.
Unsafe response parsing may throw on unexpected format.
Line 153 accesses data['choices'][0]['message']['content'] without null checks. If the API returns an unexpected format (e.g., empty choices array), this will throw a runtime exception.
🛡️ Suggested defensive parsing
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
- final aiReply = data['choices'][0]['message']['content'] as String;
+ final choices = data['choices'] as List?;
+ if (choices == null || choices.isEmpty) {
+ throw Exception('Invalid response: no choices returned');
+ }
+ final message = choices[0]['message'] as Map<String, dynamic>?;
+ final aiReply = message?['content'] as String?;
+ if (aiReply == null) {
+ throw Exception('Invalid response: no content in message');
+ }
// save AI's response for context
_conversationHistory.add(ChatMessage(role: 'assistant', content: aiReply));📝 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.
| if (response.statusCode == 200) { | |
| final data = jsonDecode(response.body); | |
| final aiReply = data['choices'][0]['message']['content'] as String; | |
| // save AI's response for context | |
| _conversationHistory.add(ChatMessage(role: 'assistant', content: aiReply)); | |
| return aiReply; | |
| } else { | |
| final err = jsonDecode(response.body); | |
| throw Exception(err['error']?['message'] ?? 'Something went wrong'); | |
| } | |
| if (response.statusCode == 200) { | |
| final data = jsonDecode(response.body); | |
| final choices = data['choices'] as List?; | |
| if (choices == null || choices.isEmpty) { | |
| throw Exception('Invalid response: no choices returned'); | |
| } | |
| final message = choices[0]['message'] as Map<String, dynamic>?; | |
| final aiReply = message?['content'] as String?; | |
| if (aiReply == null) { | |
| throw Exception('Invalid response: no content in message'); | |
| } | |
| // save AI's response for context | |
| _conversationHistory.add(ChatMessage(role: 'assistant', content: aiReply)); | |
| return aiReply; | |
| } else { | |
| final err = jsonDecode(response.body); | |
| throw Exception(err['error']?['message'] ?? 'Something went wrong'); | |
| } |
🤖 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 151 - 161, The code
reads data['choices'][0]['message']['content'] without validating structure
which can throw if choices is missing/empty; update the success-path in the
response.statusCode == 200 branch to defensively parse: check that data is a
Map, that data['choices'] is a non-empty List, and that
choices[0]['message']['content'] is a String (or provide a safe fallback like
'No response'); only then create aiReply, add ChatMessage(role: 'assistant',
content: aiReply) to _conversationHistory, otherwise log the unexpected payload
and throw a clear Exception indicating malformed API response so callers don’t
get silent runtime exceptions.
| # To add assets to your application, add an assets section, like this: | ||
| assets: | ||
| - assets/images/ | ||
| - .env |
There was a problem hiding this comment.
Security risk: .env file bundled into app binary.
Including .env in Flutter assets embeds it into the compiled APK/IPA, making any secrets (like OPENROUTER_API_KEY) extractable by anyone who decompiles the app. This defeats the purpose of using a .env file for secret management.
For mobile apps, API keys that need to remain secret should be:
- Proxied through a backend service you control, or
- Use OAuth flows where the client never holds the secret, or
- Accept that client-side keys are public (only suitable for rate-limited/free-tier keys with no billing impact)
If the OpenRouter key is free-tier with no billing risk, document this decision. Otherwise, consider implementing a backend proxy.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter_app/pubspec.yaml` at line 70, The .env file is currently listed as a
Flutter asset in pubspec.yaml (the ".env" entry) which bundles secrets like
OPENROUTER_API_KEY into the app; remove the ".env" entry from the assets list in
pubspec.yaml so it is not included in the compiled APK/IPA, add .env to
.gitignore to avoid committing it, and migrate secret usage to a secure pattern
(e.g., proxy requests through your backend or use OAuth) or—if you accept a
public client-side key—document that decision and switch to safer build-time
injection (e.g., dart-define) instead of bundling .env as an asset.
There was a problem hiding this comment.
openrouter key is free tier
There was a problem hiding this comment.
@Ashish-Kumar-Dash, thanks for clarifying! Since the key is free-tier with no billing impact, the risk is significantly reduced.
That said, a few residual considerations remain:
- Rate-limit exhaustion: Bad actors extracting the key from the APK could consume your free quota, causing the chatbot to stop working for real users.
- Key revocation: OpenRouter may revoke keys found to be abused in the wild.
Since you've accepted these risks for a free-tier key, I'd recommend documenting this decision in your README.md so future contributors understand why .env is bundled as an asset rather than wondering if it's a mistake. Something like:
> **Note on API Key:** The OpenRouter API key used for the AI chatbot is a free-tier key intentionally bundled in the app. There is no billing risk, but if the key is abused, replace it in `.env`.This matches the guidance I originally noted — as long as the decision is documented, this is an acceptable approach for a free-tier key. ✅
Integrates AI-powered mental health chatbot using OpenRouter (Step 3.5 Flash) and adds patient onboarding with medical history collection.
Changes
AI Chatbot: Real API integration with OpenRouter, conversation history, loading states, error handling
Onboarding: 3-step flow collecting basic info, medical history, emergency contacts with privacy notice
Profile Page: View/edit patient details with unique display ID
Context-aware AI: Chatbot uses patient's medical history for personalized responses
Auth Flow: Users must complete onboarding before accessing home
Summary by CodeRabbit
Release Notes
New Features