diff --git a/.env.example b/.env.example deleted file mode 100644 index 2329a2c..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -GEMINI_API_KEY=your_gemini_api_key_here -DEEPGRAM_API_KEY=your_deepgram_api_key_here \ No newline at end of file diff --git a/lib/features/transcription/data/deepgram_service.dart b/lib/features/transcription/data/deepgram_service.dart index 4969ca8..baf7cbc 100644 --- a/lib/features/transcription/data/deepgram_service.dart +++ b/lib/features/transcription/data/deepgram_service.dart @@ -14,7 +14,6 @@ class DeepgramService { if (configuredKey != null && configuredKey.isNotEmpty) { return configuredKey; } - try { return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim(); } catch (_) { @@ -22,62 +21,82 @@ class DeepgramService { } } - Future transcribe(String recordingPath) async { - final apiKey = _resolveApiKey(); - if (apiKey.isEmpty) { - throw Exception('Missing DEEPGRAM_API_KEY in environment'); + Future _retryPost({ + required Uri uri, + required Map headers, + required List body, + int retries = 3, + }) async { + for (int attempt = 0; attempt < retries; attempt++) { + try { + final response = await http + .post(uri, headers: headers, body: body) + .timeout(const Duration(seconds: 30)); + + if (response.statusCode == 200) return response; + + if (response.statusCode >= 500) { + await Future.delayed(Duration(seconds: 2 * (attempt + 1))); + continue; + } else { + throw Exception('Deepgram failed: ${response.statusCode} - ${response.body}'); + } + } on TimeoutException { + if (attempt == retries - 1) throw Exception('Request timed out'); + } catch (e) { + if (attempt == retries - 1) rethrow; + } + await Future.delayed(Duration(seconds: 2 * (attempt + 1))); } + throw Exception('Failed after $retries retries'); + } - final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2'); + Future transcribe(String recordingPath) async { + final apiKey = _resolveApiKey(); + if (apiKey.isEmpty) throw Exception('Missing DEEPGRAM_API_KEY'); final file = File(recordingPath); - if (!await file.exists()) { - throw Exception('Recording file not found'); - } + if (!await file.exists()) throw Exception('Recording file not found'); final bytes = await file.readAsBytes(); + final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&smart_format=true'); - http.Response response; - try { - response = await http.post( - uri, - headers: { - 'Authorization': 'Token $apiKey', - 'Content-Type': 'audio/m4a', - }, - body: bytes, - ).timeout(const Duration(seconds: 30)); - } on TimeoutException { - throw Exception('Deepgram request timed out after 30 seconds'); - } - - if (response.statusCode == 200) { - final decodedResponse = json.decode(response.body); + final response = await _retryPost( + uri: uri, + headers: { + 'Authorization': 'Token $apiKey', + 'Content-Type': 'application/octet-stream', + }, + body: bytes, + ); - if (decodedResponse is! Map) { - throw Exception('Deepgram returned unexpected response format'); - } + return _parseTranscript(response.body); + } - final results = decodedResponse['results']; - if (results is! Map) { - return 'No speech detected'; - } + String _parseTranscript(String responseBody) { + try { + final decoded = json.decode(responseBody); - final channels = results['channels']; - if (channels is! List || channels.isEmpty || channels.first is! Map) { + if (decoded is! Map) { return 'No speech detected'; } - final alternatives = (channels.first as Map)['alternatives']; - if (alternatives is! List || alternatives.isEmpty || alternatives.first is! Map) { - return 'No speech detected'; + final results = decoded['results']; + final channels = results?['channels']; + + if (channels is List && channels.isNotEmpty) { + final alternatives = channels[0]['alternatives']; + if (alternatives is List && alternatives.isNotEmpty) { + final transcript = alternatives[0]['transcript']; + if (transcript is String && transcript.trim().isNotEmpty) { + return transcript.trim(); + } + } } - final transcript = (alternatives.first as Map)['transcript']; - final result = transcript is String ? transcript.trim() : ''; - return result.isNotEmpty ? result : 'No speech detected'; - } else { - throw Exception('Deepgram failed: ${response.statusCode}'); + return 'No speech detected'; + } catch (e) { + throw Exception('Failed to parse Deepgram response: $e'); } } -} \ No newline at end of file +} diff --git a/lib/features/transcription/data/gemini_service.dart b/lib/features/transcription/data/gemini_service.dart index 34ab854..235cac0 100644 --- a/lib/features/transcription/data/gemini_service.dart +++ b/lib/features/transcription/data/gemini_service.dart @@ -1,18 +1,45 @@ +import 'dart:convert'; +import '../domain/medical_insights.dart'; import 'package:doc_pilot_new_app_gradel_fix/services/chatbot_service.dart'; class GeminiService { final ChatbotService _chatbotService = ChatbotService(); - Future generateSummary(String transcription) async { - return await _chatbotService.getGeminiResponse( - "Generate a summary of the conversation based on this transcription: $transcription", - ); + Future generateInsights(String transcription) async { + final prompt = """ +Extract structured medical information from the conversation. + +Return ONLY valid JSON in this format: +{ + "summary": "short summary", + "symptoms": ["symptom1", "symptom2"], + "medicines": ["medicine1", "medicine2"] +} + +Conversation: +$transcription +"""; + + final response = await _chatbotService.getGeminiResponse(prompt); + + try { + final cleaned = _extractJson(response); + final jsonData = json.decode(cleaned); + return MedicalInsights.fromJson(jsonData); + } catch (e) { + throw Exception('Failed to parse Gemini JSON response'); + } } - Future generatePrescription(String transcription) async { - await Future.delayed(const Duration(seconds: 3)); - return await _chatbotService.getGeminiResponse( - "Generate a prescription based on the conversation in this transcription: $transcription", - ); + // Handles messy AI responses + String _extractJson(String response) { + final start = response.indexOf('{'); + final end = response.lastIndexOf('}'); + + if (start != -1 && end != -1) { + return response.substring(start, end + 1); + } + + throw Exception('Invalid JSON format from AI'); } } \ No newline at end of file diff --git a/lib/features/transcription/domain/medical_insights.dart b/lib/features/transcription/domain/medical_insights.dart new file mode 100644 index 0000000..44984b9 --- /dev/null +++ b/lib/features/transcription/domain/medical_insights.dart @@ -0,0 +1,39 @@ +class MedicalInsights { + final String summary; + final List symptoms; + final List medicines; + + MedicalInsights({ + required this.summary, + required this.symptoms, + required this.medicines, + }); + + factory MedicalInsights.fromJson(Map json) { + return MedicalInsights( + // Coerce summary to String regardless of what the AI sends + summary: json['summary']?.toString() ?? '', + + // Safely parse lists to avoid 'type is not a subtype' errors + symptoms: _parseList(json['symptoms']), + medicines: _parseList(json['medicines']), + ); + } + + /// Helper to filter nulls and force elements to strings + static List _parseList(dynamic jsonValue) { + if (jsonValue is! List) return []; + return jsonValue + .where((item) => item != null) + .map((item) => item.toString()) + .toList(); + } + + Map toJson() { + return { + 'summary': summary, + 'symptoms': symptoms, + 'medicines': medicines, + }; + } +} diff --git a/lib/features/transcription/domain/transcription_model.dart b/lib/features/transcription/domain/transcription_model.dart index 5310208..980ce65 100644 --- a/lib/features/transcription/domain/transcription_model.dart +++ b/lib/features/transcription/domain/transcription_model.dart @@ -1,23 +1,21 @@ +import 'medical_insights.dart'; + class TranscriptionModel { final String rawTranscript; - final String summary; - final String prescription; + final MedicalInsights? insights; const TranscriptionModel({ this.rawTranscript = '', - this.summary = '', - this.prescription = '', + this.insights, }); TranscriptionModel copyWith({ String? rawTranscript, - String? summary, - String? prescription, + MedicalInsights? insights, }) { return TranscriptionModel( rawTranscript: rawTranscript ?? this.rawTranscript, - summary: summary ?? this.summary, - prescription: prescription ?? this.prescription, + insights: insights ?? this.insights, ); } } \ No newline at end of file diff --git a/lib/features/transcription/presentation/transcription_controller.dart b/lib/features/transcription/presentation/transcription_controller.dart index 8fddbd7..33b414a 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -8,6 +8,7 @@ import 'package:permission_handler/permission_handler.dart'; import '../data/deepgram_service.dart'; import '../data/gemini_service.dart'; import '../domain/transcription_model.dart'; +import '../domain/medical_insights.dart'; enum TranscriptionState { idle, recording, transcribing, processing, done, error } @@ -18,34 +19,34 @@ class TranscriptionController extends ChangeNotifier { TranscriptionState state = TranscriptionState.idle; TranscriptionModel data = const TranscriptionModel(); + String? errorMessage; String _recordingPath = ''; - // Waveform — kept here since it's driven by recording state final List waveformValues = List.filled(40, 0.0); - Timer? _waveformTimer; + Timer? _waveformTimer; bool get isRecording => state == TranscriptionState.recording; bool get isProcessing => state == TranscriptionState.transcribing || state == TranscriptionState.processing; String get transcription => data.rawTranscript; - String get summary => data.summary; - String get prescription => data.prescription; + String get summary => data.insights?.summary ?? ''; - Future requestPermissions() async { - final status = await Permission.microphone.request(); + List get symptoms => List.unmodifiable(data.insights?.symptoms ?? []); + List get medicines => List.unmodifiable(data.insights?.medicines ?? []); - if (status.isGranted) { - return true; - } + @Deprecated('Use summary, symptoms, or medicines instead') + String get prescription => summary; + Future requestPermissions() async { + final status = await Permission.microphone.request(); + if (status.isGranted) return true; if (status.isPermanentlyDenied) { _setError('Microphone permission permanently denied. Please enable it in settings.'); return false; } - _setError('Microphone permission denied'); return false; } @@ -62,15 +63,11 @@ class TranscriptionController extends ChangeNotifier { try { if (!await _audioRecorder.hasPermission()) { final granted = await requestPermissions(); - if (!granted) { - return; - } + if (!granted) return; } - final directory = await getTemporaryDirectory(); _recordingPath = '${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.m4a'; - await _audioRecorder.start( RecordConfig( encoder: AudioEncoder.aacLc, @@ -79,13 +76,10 @@ class TranscriptionController extends ChangeNotifier { ), path: _recordingPath, ); - - // Reset previous data data = const TranscriptionModel(); state = TranscriptionState.recording; _startWaveformAnimation(); notifyListeners(); - developer.log('Started recording to: $_recordingPath'); } catch (e) { _setError('Error starting recording: $e'); @@ -96,11 +90,9 @@ class TranscriptionController extends ChangeNotifier { try { _waveformTimer?.cancel(); _resetWaveform(); - await _audioRecorder.stop(); state = TranscriptionState.transcribing; notifyListeners(); - developer.log('Recording stopped, transcribing...'); await _transcribe(); } catch (e) { @@ -111,11 +103,9 @@ class TranscriptionController extends ChangeNotifier { Future _transcribe() async { try { final transcript = await _deepgramService.transcribe(_recordingPath); - data = data.copyWith(rawTranscript: transcript); state = TranscriptionState.processing; notifyListeners(); - if (transcript.isNotEmpty && transcript != 'No speech detected') { await _processWithGemini(transcript); } else { @@ -129,14 +119,12 @@ class TranscriptionController extends ChangeNotifier { Future _processWithGemini(String transcript) async { try { - final summary = await _geminiService.generateSummary(transcript); - final prescription = await _geminiService.generatePrescription(transcript); - - data = data.copyWith(summary: summary, prescription: prescription); + final MedicalInsights insights = + await _geminiService.generateInsights(transcript); + data = data.copyWith(insights: insights); state = TranscriptionState.done; notifyListeners(); - - developer.log('Gemini processing complete'); + developer.log('Gemini structured insights generated'); } catch (e) { _setError('Gemini error: $e'); } @@ -164,10 +152,16 @@ class TranscriptionController extends ChangeNotifier { developer.log(message); } + void checkConfigStatus(bool isLoaded) { + if (!isLoaded) { + _setError('Configuration Error: API keys could not be loaded. Please check your .env file.'); + } + } + @override void dispose() { _waveformTimer?.cancel(); _audioRecorder.dispose(); super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index a957182..0c33565 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -1,6 +1,6 @@ -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'; +import 'package:doc_pilot_new_app_gradel_fix/screens/medical_insights_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'transcription_controller.dart'; @@ -32,7 +32,11 @@ class TranscriptionScreen extends StatelessWidget { children: [ const Text( 'DocPilot', - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white), + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), const SizedBox(height: 8), Text( @@ -57,7 +61,12 @@ class TranscriptionScreen extends StatelessWidget { height: value * 80 + 5, decoration: BoxDecoration( color: controller.isRecording - ? HSLColor.fromAHSL(1.0, (280 + index * 2) % 360, 0.8, 0.7 + value * 0.2).toColor() + ? HSLColor.fromAHSL( + 1.0, + (280 + index * 2) % 360, + 0.8, + 0.7 + value * 0.2, + ).toColor() : Colors.white.withOpacity(0.5), borderRadius: BorderRadius.circular(5), ), @@ -66,38 +75,48 @@ class TranscriptionScreen extends StatelessWidget { ), ), ), + const SizedBox(height: 40), - // Mic button + // Mic Button Center( child: GestureDetector( onTap: controller.isProcessing ? null : controller.toggleRecording, - child: Container( - width: 100, - height: 100, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: controller.isRecording ? Colors.red : Colors.white, - boxShadow: [ - BoxShadow( - color: (controller.isRecording ? Colors.red : Colors.white).withOpacity(0.3), - spreadRadius: 8, - blurRadius: 20, - ), - ], - ), - child: Icon( - controller.isRecording ? Icons.stop : Icons.mic, - size: 50, - color: controller.isRecording ? Colors.white : Colors.deepPurple.shade800, + child: AnimatedScale( + scale: controller.isRecording ? 1.2 : 1.0, + duration: const Duration(milliseconds: 200), + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: controller.isRecording ? Colors.red : Colors.white, + boxShadow: [ + BoxShadow( + color: (controller.isRecording ? Colors.red : Colors.white) + .withOpacity(0.3), + spreadRadius: 8, + blurRadius: 20, + ), + ], + ), + child: Icon( + controller.isRecording ? Icons.stop : Icons.mic, + size: 50, + color: controller.isRecording + ? Colors.white + : Colors.deepPurple.shade800, + ), ), ), ), ), + const SizedBox(height: 20), - // Status indicator - Center( + // Status Row + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -105,52 +124,97 @@ class TranscriptionScreen extends StatelessWidget { Container( width: 16, height: 16, - margin: const EdgeInsets.only(right: 8.0), + margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( shape: BoxShape.circle, color: controller.isRecording ? Colors.red : controller.state == TranscriptionState.processing - ? Colors.blue - : Colors.amber, + ? Colors.blue + : Colors.amber, + ), + ), + Expanded( + child: Text( + _statusDetailText(controller), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, ), ), - Text( - _statusDetailText(controller), - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white), ), ], ), ), - const SizedBox(height: 40), - // Navigation buttons + const SizedBox(height: 20), + + // Empty State (FIXED: No period in sentinel string) + if (controller.state == TranscriptionState.done && + (controller.transcription.isEmpty || + controller.transcription == "No speech detected")) + const Center( + child: Text( + "No speech detected. Try again.", + style: TextStyle(color: Colors.white70), + ), + ), + + const SizedBox(height: 20), + + // Navigation Buttons Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: ListView( children: [ _buildNavigationButton( - context, 'Transcription', Icons.record_voice_over, - controller.transcription.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => TranscriptionDetailScreen(transcription: controller.transcription), - )), + context, + 'Summary', + Icons.description, + controller.summary.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => SummaryScreen(summary: controller.summary)) + ), ), - const SizedBox(height: 16), + + const SizedBox(height: 12), + _buildNavigationButton( - context, 'Summary', Icons.summarize, - controller.summary.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => SummaryScreen(summary: controller.summary), - )), + context, + 'Full Transcription', + Icons.text_snippet, + controller.transcription.isNotEmpty && + controller.transcription != "No speech detected" && + !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => TranscriptionDetailScreen( + transcription: controller.transcription, + ), + ), + ), ), - const SizedBox(height: 16), + + const SizedBox(height: 12), + _buildNavigationButton( - context, 'Prescription', Icons.medication, - controller.prescription.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => PrescriptionScreen(prescription: controller.prescription), - )), + context, + 'Medical Insights', + Icons.analytics, + controller.summary.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MedicalInsightsScreen( + symptoms: controller.symptoms, + medicines: controller.medicines, + ), + ), + ), ), ], ), @@ -163,56 +227,74 @@ class TranscriptionScreen extends StatelessWidget { ); } - String _statusText(TranscriptionState state) { - switch (state) { - case TranscriptionState.recording: return 'Recording your voice...'; - case TranscriptionState.transcribing: return 'Transcribing your voice...'; - case TranscriptionState.processing: return 'Processing with Gemini...'; - case TranscriptionState.error: return 'Something went wrong'; - default: return 'Tap the mic to begin'; - } - } - - String _statusDetailText(TranscriptionController controller) { - switch (controller.state) { - case TranscriptionState.recording: return 'Recording in progress'; - case TranscriptionState.transcribing: return 'Processing audio...'; - case TranscriptionState.processing: return 'Generating content with Gemini...'; - case TranscriptionState.done: return 'Ready to view results'; - case TranscriptionState.error: return controller.errorMessage ?? 'Error occurred'; - default: return 'Press the microphone button to start'; - } - } - Widget _buildNavigationButton( BuildContext context, - String title, + String label, IconData icon, - bool isEnabled, - VoidCallback onPressed, + bool enabled, + VoidCallback onTap, ) { - return SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: isEnabled ? onPressed : null, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: Colors.white, - foregroundColor: Colors.deepPurple, - disabledBackgroundColor: Colors.white.withOpacity(0.3), - disabledForegroundColor: Colors.white.withOpacity(0.5), - elevation: isEnabled ? 4 : 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: 24), - const SizedBox(width: 12), - Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - ], + return Material( + color: Colors.transparent, + child: InkWell( + onTap: enabled ? onTap : null, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: enabled ? Colors.white.withOpacity(0.15) : Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: enabled ? Colors.white24 : Colors.white10, + ), + ), + child: Row( + children: [ + Icon(icon, color: enabled ? Colors.white : Colors.white38), + const SizedBox(width: 16), + Text( + label, + style: TextStyle( + color: enabled ? Colors.white : Colors.white38, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + Icon( + Icons.arrow_forward_ios, + size: 16, + color: enabled ? Colors.white70 : Colors.white12, + ), + ], + ), ), ), ); } -} \ No newline at end of file + + String _statusText(TranscriptionState state) { + switch (state) { + case TranscriptionState.recording: + return 'Recording your voice...'; + case TranscriptionState.transcribing: + return 'Transcribing your voice...'; + case TranscriptionState.processing: + return 'Processing with Gemini...'; + case TranscriptionState.done: + return 'Transcription complete!'; + case TranscriptionState.error: + return 'Something went wrong'; + default: + return 'Tap the mic to begin'; + } + } + + String _statusDetailText(TranscriptionController controller) { + if (controller.isRecording) return 'Recording in progress...'; + if (controller.state == TranscriptionState.transcribing) return 'Converting speech to text...'; + if (controller.state == TranscriptionState.processing) return 'Extracting medical insights...'; + if (controller.state == TranscriptionState.done) return 'Review your insights below'; + return 'Tap the microphone to begin'; + } +} diff --git a/lib/main.dart b/lib/main.dart index c913ee0..bf7a6d4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,22 +1,37 @@ import 'dart:async'; -import 'package:doc_pilot_new_app_gradel_fix/features/transcription/presentation/transcription_controller.dart'; -import 'package:doc_pilot_new_app_gradel_fix/features/transcription/presentation/transcription_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; +import 'features/transcription/presentation/transcription_controller.dart'; +import 'features/transcription/presentation/transcription_screen.dart'; + Future main() async { - await dotenv.load(); - runApp(const MyApp()); + WidgetsFlutterBinding.ensureInitialized(); + + bool isConfigLoaded = false; + try { + // Attempt to load environment variables + await dotenv.load(fileName: ".env").timeout(const Duration(seconds: 2)); + isConfigLoaded = true; + } catch (e) { + debugPrint("Critical: Could not load .env file: $e"); + // We proceed to runApp so we can show a user-friendly error in the UI + } + + runApp(MyApp(isConfigLoaded: isConfigLoaded)); } class MyApp extends StatelessWidget { - const MyApp({super.key}); + final bool isConfigLoaded; + + const MyApp({super.key, required this.isConfigLoaded}); @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (_) => TranscriptionController(), + // Pass the config status to the controller so it can show an alert + create: (_) => TranscriptionController()..checkConfigStatus(isConfigLoaded), child: MaterialApp( title: 'DocPilot', theme: ThemeData( diff --git a/lib/screens/medical_insights_screen.dart b/lib/screens/medical_insights_screen.dart new file mode 100644 index 0000000..80aa32f --- /dev/null +++ b/lib/screens/medical_insights_screen.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class MedicalInsightsScreen extends StatelessWidget { + final List symptoms; + final List medicines; + + const MedicalInsightsScreen({ + super.key, + required this.symptoms, + required this.medicines, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Medical Insights")), + body: Padding( + padding: const EdgeInsets.all(16), + child: ListView( + children: [ + const Text( + "Symptoms", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + if (symptoms.isEmpty) + const Text("No symptoms detected") + else + ...symptoms.map( + (e) => ListTile( + leading: const Icon(Icons.warning, color: Colors.orange), + title: Text(e), + ), + ), + + const SizedBox(height: 20), + + const Text( + "Medicines", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + if (medicines.isEmpty) + const Text("No medicines suggested") + else + ...medicines.map( + (e) => ListTile( + leading: const Icon(Icons.medication, color: Colors.blue), + title: Text(e), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 2ddb4f7..359c186 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -164,26 +164,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + 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,26 +204,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -232,6 +232,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -352,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" record: dependency: "direct main" description: @@ -481,10 +497,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.10" typed_data: dependency: transitive description: @@ -537,10 +553,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 +590,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 49ef91d..52c287a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,6 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: - # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see