diff --git a/docs/screenshots/mobile-overflow-issue.png b/docs/screenshots/mobile-overflow-issue.png new file mode 100644 index 0000000..b4e59b1 Binary files /dev/null and b/docs/screenshots/mobile-overflow-issue.png differ diff --git a/lib/main.dart b/lib/main.dart index 1013f29..894a1b6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,6 @@ import 'package:record/record.dart'; import 'package:http/http.dart' as http; import 'package:permission_handler/permission_handler.dart'; import 'dart:developer' as developer; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'services/chatbot_service.dart'; @@ -17,7 +16,11 @@ import 'screens/summary_screen.dart'; import 'screens/prescription_screen.dart'; Future main() async { - await dotenv.load(); + try { + await dotenv.load(fileName: '.env'); + } catch (_) { + await dotenv.load(fileName: '.env.example'); + } runApp(const MyApp()); } @@ -67,6 +70,25 @@ class _TranscriptionScreenState extends State with SingleTi final List _waveformValues = List.filled(40, 0.0); Timer? _waveformTimer; + bool _isValidApiKey(String value, String provider) { + final trimmed = value.trim(); + if (trimmed.isEmpty) { + return false; + } + + final normalized = trimmed.toLowerCase(); + if (provider == 'deepgram') { + return !normalized.contains('your_deepgram_api_key_here') && + !normalized.contains('replace_with') && + !normalized.contains('example') && + !normalized.contains('dummy'); + } + + return !normalized.contains('replace_with') && + !normalized.contains('example') && + !normalized.contains('dummy'); + } + @override void initState() { super.initState(); @@ -187,6 +209,15 @@ class _TranscriptionScreenState extends State with SingleTi Future _transcribeAudio() async { try { final apiKey = dotenv.env['DEEPGRAM_API_KEY'] ?? ''; + if (!_isValidApiKey(apiKey, 'deepgram')) { + setState(() { + _isTranscribing = false; + _isProcessing = false; + _transcription = 'DEEPGRAM_API_KEY is missing or still a placeholder. Add a real key to .env'; + }); + return; + } + final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2'); final file = File(_recordingPath); @@ -224,6 +255,15 @@ class _TranscriptionScreenState extends State with SingleTi print(_transcription); print('============================================='); + if (!_chatbotService.hasValidApiKey) { + setState(() { + _isProcessing = false; + _summaryContent = 'Error: GEMINI_API_KEY is missing. Add it to your .env file.'; + _prescriptionContent = 'Error: GEMINI_API_KEY is missing. Add it to your .env file.'; + }); + return; + } + // Send to Gemini for processing if we have a valid transcription if (_transcription.isNotEmpty && _transcription != 'No speech detected') { await _processWithGemini(_transcription); @@ -233,9 +273,18 @@ class _TranscriptionScreenState extends State with SingleTi }); } } else { + String message = 'Transcription failed (status ${response.statusCode})'; + try { + final decodedError = json.decode(response.body); + final errorText = decodedError['error'] ?? decodedError['message']; + if (errorText is String && errorText.trim().isNotEmpty) { + message = 'Transcription failed: $errorText'; + } + } catch (_) {} + setState(() { _isTranscribing = false; - _transcription = 'Transcription failed'; + _transcription = message; _isProcessing = false; }); } @@ -296,6 +345,11 @@ class _TranscriptionScreenState extends State with SingleTi @override Widget build(BuildContext context) { + final deepgramKey = dotenv.env['DEEPGRAM_API_KEY'] ?? ''; + final hasDeepgramKey = _isValidApiKey(deepgramKey, 'deepgram'); + final hasGeminiKey = _chatbotService.hasValidApiKey; + final setupComplete = hasDeepgramKey && hasGeminiKey; + return Scaffold( body: Container( decoration: BoxDecoration( @@ -309,9 +363,10 @@ class _TranscriptionScreenState extends State with SingleTi ), ), child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), - child: Column( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // App header @@ -331,12 +386,36 @@ class _TranscriptionScreenState extends State with SingleTi ? 'Transcribing your voice...' : _isProcessing ? 'Processing with Gemini...' - : 'Tap the mic to begin', + : setupComplete + ? 'Tap the mic to begin' + : 'Complete API setup to enable recording and AI output', style: const TextStyle( fontSize: 16, color: Colors.white70, ), ), + if (!setupComplete) ...[ + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.14), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + 'Missing valid API keys:\n' + '- DEEPGRAM_API_KEY (${hasDeepgramKey ? 'OK' : 'Missing/placeholder'})\n' + '- GEMINI_API_KEY (${hasGeminiKey ? 'OK' : 'Missing/placeholder'})\n\n' + 'Create .env in the project root with real keys.', + style: const TextStyle( + fontSize: 13, + color: Colors.white, + height: 1.3, + ), + ), + ), + ], const SizedBox(height: 30), // Waveform visualization @@ -380,7 +459,20 @@ class _TranscriptionScreenState extends State with SingleTi // Microphone button Center( child: GestureDetector( - onTap: (_isTranscribing || _isProcessing) ? null : _toggleRecording, + onTap: (_isTranscribing || _isProcessing) + ? null + : () { + if (!hasDeepgramKey) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Add a valid DEEPGRAM_API_KEY in .env first.'), + backgroundColor: Colors.red, + ), + ); + return; + } + _toggleRecording(); + }, child: Container( width: 100, height: 100, @@ -449,56 +541,55 @@ class _TranscriptionScreenState extends State with SingleTi const SizedBox(height: 40), // Vertical navigation buttons - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildNavigationButton( + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildNavigationButton( + context, + 'Transcription', + Icons.record_voice_over, + _formattedTranscription.isNotEmpty, + () => Navigator.push( context, - 'Transcription', - Icons.record_voice_over, - _formattedTranscription.isNotEmpty, - () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TranscriptionDetailScreen(transcription: _formattedTranscription), - ), + MaterialPageRoute( + builder: (context) => TranscriptionDetailScreen(transcription: _formattedTranscription), ), ), - const SizedBox(height: 16), - _buildNavigationButton( + ), + const SizedBox(height: 16), + _buildNavigationButton( + context, + 'Summary', + Icons.summarize, + _summaryContent.isNotEmpty, + () => Navigator.push( context, - 'Summary', - Icons.summarize, - _summaryContent.isNotEmpty, - () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SummaryScreen(summary: _summaryContent), - ), + MaterialPageRoute( + builder: (context) => SummaryScreen(summary: _summaryContent), ), ), - const SizedBox(height: 16), - _buildNavigationButton( + ), + const SizedBox(height: 16), + _buildNavigationButton( + context, + 'Prescription', + Icons.medication, + _prescriptionContent.isNotEmpty, + () => Navigator.push( context, - 'Prescription', - Icons.medication, - _prescriptionContent.isNotEmpty, - () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PrescriptionScreen(prescription: _prescriptionContent), - ), + MaterialPageRoute( + builder: (context) => PrescriptionScreen(prescription: _prescriptionContent), ), ), - ], - ), + ), + ], ), ], ), ), ), ), + ), ); } diff --git a/lib/services/chatbot_service.dart b/lib/services/chatbot_service.dart index 22d00b5..bb50479 100644 --- a/lib/services/chatbot_service.dart +++ b/lib/services/chatbot_service.dart @@ -7,10 +7,28 @@ class ChatbotService { // Get API key from .env file final String apiKey = dotenv.env['GEMINI_API_KEY'] ?? ''; + bool get hasValidApiKey => _isValidApiKey(apiKey); + + bool _isValidApiKey(String value) { + final trimmed = value.trim(); + if (trimmed.isEmpty) { + return false; + } + + final normalized = trimmed.toLowerCase(); + return !normalized.contains('your_gemini_api_key_here') && + !normalized.contains('replace_with') && + !normalized.contains('example') && + !normalized.contains('dummy'); + } + // Get a response from Gemini based on a prompt Future getGeminiResponse(String prompt) async { - print('\n=== GEMINI PROMPT ==='); - print(prompt); + if (!hasValidApiKey) { + return 'Error: GEMINI_API_KEY is missing. Add it to your .env file.'; + } + + developer.log('Sending prompt to Gemini', name: 'ChatbotService'); final url = Uri.parse('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=$apiKey'); @@ -25,23 +43,47 @@ class ChatbotService { "maxOutputTokens": 1024 } }), - ); + ).timeout(const Duration(seconds: 30)); if (response.statusCode == 200) { final data = jsonDecode(response.body); - final result = data['candidates'][0]['content']['parts'][0]['text']; - - print('\n=== GEMINI RESPONSE ==='); - print(result); + final candidates = data['candidates']; + if (candidates is List && candidates.isNotEmpty) { + final content = candidates[0]['content']; + final parts = content?['parts']; + if (parts is List && parts.isNotEmpty) { + final result = parts[0]['text']; + if (result is String && result.trim().isNotEmpty) { + developer.log('Gemini response received', name: 'ChatbotService'); + return result; + } + } + } - return result; + developer.log( + 'Gemini response format was not as expected: ${response.body}', + name: 'ChatbotService', + ); + return 'Error: Received an unexpected response format from Gemini.'; } else { - print('API Error: ${response.statusCode}'); - return "Error: Could not generate response. Status code: ${response.statusCode}"; + String errorMessage = 'Error: Gemini API request failed (status ${response.statusCode}).'; + try { + final data = jsonDecode(response.body); + final apiMessage = data['error']?['message']; + if (apiMessage is String && apiMessage.trim().isNotEmpty) { + errorMessage = 'Error: $apiMessage'; + } + } catch (_) {} + + developer.log( + 'Gemini API error: ${response.statusCode} ${response.body}', + name: 'ChatbotService', + ); + return errorMessage; } } catch (e) { - print('Exception: $e'); - return "Error: Could not connect to API: $e"; + developer.log('Gemini request failed: $e', name: 'ChatbotService', error: e); + return 'Error: Could not connect to Gemini API. Please try again.'; } } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 2702b93..db5a6b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,26 +29,26 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" cross_file: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -164,26 +164,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -204,10 +204,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -220,10 +220,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" mime: dependency: transitive description: @@ -236,10 +236,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: @@ -449,18 +449,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: @@ -481,10 +481,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.7" typed_data: dependency: transitive description: @@ -537,10 +537,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -574,5 +574,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index b5092a1..0714d84 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: assets: - - .env + - .env.example # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in diff --git a/test/widget_test.dart b/test/widget_test.dart index 927ca45..fb53525 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +1,44 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'package:doc_pilot_new_app_gradel_fix/main.dart'; +import 'package:doc_pilot_new_app_gradel_fix/screens/prescription_screen.dart'; +import 'package:doc_pilot_new_app_gradel_fix/screens/summary_screen.dart'; +import 'package:doc_pilot_new_app_gradel_fix/screens/transcription_detail_screen.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + testWidgets('SummaryScreen shows fallback when summary is empty', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: SummaryScreen(summary: ''), + ), + ); + + expect(find.text('Conversation Summary'), findsOneWidget); + expect(find.text('No summary available'), findsOneWidget); + }); + + testWidgets('PrescriptionScreen renders markdown content', (WidgetTester tester) async { + const prescription = '# Prescription\n\n- Paracetamol 500mg'; + + await tester.pumpWidget( + const MaterialApp( + home: PrescriptionScreen(prescription: prescription), + ), + ); + + expect(find.text('Prescription'), findsWidgets); + expect(find.text('Paracetamol 500mg'), findsOneWidget); + }); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); + testWidgets('TranscriptionDetailScreen renders transcription text', (WidgetTester tester) async { + const transcription = 'Doctor: How are you feeling today?'; - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); + await tester.pumpWidget( + const MaterialApp( + home: TranscriptionDetailScreen(transcription: transcription), + ), + ); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + expect(find.text('Doctor-Patient Conversation'), findsOneWidget); + expect(find.text('Doctor: How are you feeling today?'), findsOneWidget); }); }