From e2b4c9a98c101c33b5d09f070759e5a77c9e8e61 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 14:53:28 +0530 Subject: [PATCH 01/17] feat: add structured medical insights with Gemini AI JSON parsing --- .env.example | 2 - .../transcription/data/deepgram_service.dart | 107 +++++++++++++----- .../transcription/data/gemini_service.dart | 45 ++++++-- .../domain/medical_insights.dart | 27 +++++ .../domain/transcription_model.dart | 14 +-- .../transcription_controller.dart | 30 ++--- lib/main.dart | 10 +- pubspec.lock | 58 ++++++---- pubspec.yaml | 4 +- 9 files changed, 211 insertions(+), 86 deletions(-) delete mode 100644 .env.example create mode 100644 lib/features/transcription/domain/medical_insights.dart 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..a6a89a5 100644 --- a/lib/features/transcription/data/deepgram_service.dart +++ b/lib/features/transcription/data/deepgram_service.dart @@ -9,6 +9,9 @@ class DeepgramService { DeepgramService({String? apiKey}) : _apiKey = apiKey?.trim(); + // ============================= + // Resolve API Key + // ============================= String _resolveApiKey() { final configuredKey = _apiKey; if (configuredKey != null && configuredKey.isNotEmpty) { @@ -22,62 +25,108 @@ class DeepgramService { } } + // ============================= + // Retry Logic (Production Grade) + // ============================= + 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; + } + + // Retry only for server errors + 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 after multiple retries'); + } + } catch (e) { + if (attempt == retries - 1) { + rethrow; + } + } + + // Exponential backoff + await Future.delayed(Duration(seconds: 2 * (attempt + 1))); + } + + throw Exception('Failed after $retries retries'); + } + + // ============================= + // Main Transcription Method + // ============================= Future transcribe(String recordingPath) async { final apiKey = _resolveApiKey(); + if (apiKey.isEmpty) { throw Exception('Missing DEEPGRAM_API_KEY in environment'); } - final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2'); - final file = File(recordingPath); + if (!await file.exists()) { throw Exception('Recording file not found'); } final bytes = await file.readAsBytes(); - http.Response response; + final uri = Uri.parse( + 'https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&smart_format=true', + ); + try { - response = await http.post( - uri, + final response = await _retryPost( + uri: uri, headers: { 'Authorization': 'Token $apiKey', - 'Content-Type': 'audio/m4a', + 'Content-Type': 'application/octet-stream', // 🔥 improved }, 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); + return _parseTranscript(response.body); + } catch (e) { + throw Exception('Transcription error: $e'); + } + } - if (decodedResponse is! Map) { - throw Exception('Deepgram returned unexpected response format'); - } + // ============================= + // Response Parser (Robust) + // ============================= + String _parseTranscript(String responseBody) { + try { + final decoded = json.decode(responseBody); - final results = decodedResponse['results']; - if (results is! Map) { + if (decoded is! Map) { return 'No speech detected'; } - final channels = results['channels']; - if (channels is! List || channels.isEmpty || channels.first is! Map) { - return 'No speech detected'; - } + final transcript = decoded['results']?['channels']?[0]?['alternatives']?[0]?['transcript']; - final alternatives = (channels.first as Map)['alternatives']; - if (alternatives is! List || alternatives.isEmpty || alternatives.first is! Map) { - return 'No speech detected'; + 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 (_) { + return 'Failed to parse transcription'; } } } \ 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..8306bb5 --- /dev/null +++ b/lib/features/transcription/domain/medical_insights.dart @@ -0,0 +1,27 @@ +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( + summary: json['summary'] ?? '', + symptoms: List.from(json['symptoms'] ?? []), + medicines: List.from(json['medicines'] ?? []), + ); + } + + Map toJson() { + return { + 'summary': summary, + 'symptoms': symptoms, + 'medicines': medicines, + }; + } +} \ No newline at end of file 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..8e1fd02 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -5,9 +5,11 @@ import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; 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 } @@ -21,25 +23,27 @@ class TranscriptionController extends ChangeNotifier { String? errorMessage; String _recordingPath = ''; - // Waveform — kept here since it's driven by recording state + // Waveform final List waveformValues = List.filled(40, 0.0); 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; + + // ✅ NEW STRUCTURED GETTERS + String get summary => data.insights?.summary ?? ''; + List get symptoms => data.insights?.symptoms ?? []; + List get medicines => data.insights?.medicines ?? []; Future requestPermissions() async { final status = await Permission.microphone.request(); - if (status.isGranted) { - return true; - } + if (status.isGranted) return true; if (status.isPermanentlyDenied) { _setError('Microphone permission permanently denied. Please enable it in settings.'); @@ -62,9 +66,7 @@ class TranscriptionController extends ChangeNotifier { try { if (!await _audioRecorder.hasPermission()) { final granted = await requestPermissions(); - if (!granted) { - return; - } + if (!granted) return; } final directory = await getTemporaryDirectory(); @@ -80,7 +82,6 @@ class TranscriptionController extends ChangeNotifier { path: _recordingPath, ); - // Reset previous data data = const TranscriptionModel(); state = TranscriptionState.recording; _startWaveformAnimation(); @@ -127,16 +128,17 @@ class TranscriptionController extends ChangeNotifier { } } + // ✅ UPDATED: Structured AI Processing Future _processWithGemini(String transcript) async { try { - final summary = await _geminiService.generateSummary(transcript); - final prescription = await _geminiService.generatePrescription(transcript); + final MedicalInsights insights = + await _geminiService.generateInsights(transcript); - data = data.copyWith(summary: summary, prescription: prescription); + 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'); } diff --git a/lib/main.dart b/lib/main.dart index c913ee0..48544bc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,10 +6,18 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; Future main() async { - await dotenv.load(); + WidgetsFlutterBinding.ensureInitialized(); + + try { + await dotenv.load(fileName: ".env").timeout(const Duration(seconds: 2)); + } catch (e) { + debugPrint("Warning: Could not load .env file: $e"); + } + runApp(const MyApp()); } + class MyApp extends StatelessWidget { const MyApp({super.key}); 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..6bfa8df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,8 +64,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg + assets: + - .env # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see From 5aa0dd4fbb66f3fee60aa973454ca8028659a55b Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 15:59:07 +0530 Subject: [PATCH 02/17] update transcription_screen file --- .../presentation/transcription_screen.dart | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index a957182..02d2a48 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -10,6 +10,7 @@ class TranscriptionScreen extends StatelessWidget { @override Widget build(BuildContext context) { + // Listen to changes in the controller final controller = context.watch(); return Scaffold( @@ -32,7 +33,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( @@ -41,7 +46,7 @@ class TranscriptionScreen extends StatelessWidget { ), const SizedBox(height: 30), - // Waveform + // Waveform Display SizedBox( height: 100, child: Row( @@ -96,7 +101,7 @@ class TranscriptionScreen extends StatelessWidget { ), const SizedBox(height: 20), - // Status indicator + // Status indicator with Overflow Fix Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -111,20 +116,28 @@ class TranscriptionScreen extends StatelessWidget { color: controller.isRecording ? Colors.red : controller.state == TranscriptionState.processing - ? Colors.blue - : Colors.amber, + ? Colors.blue + : Colors.amber, + ), + ), + // FIX: Wrapped in Expanded to prevent the 174px right overflow + Expanded( + child: Text( + _statusDetailText(controller), + textAlign: TextAlign.center, + 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 + // Navigation buttons with Compilation Fix Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -145,11 +158,14 @@ class TranscriptionScreen extends StatelessWidget { )), ), const SizedBox(height: 16), + // FIX: Using controller.medicines instead of controller.prescription _buildNavigationButton( context, 'Prescription', Icons.medication, - controller.prescription.isNotEmpty, + controller.medicines.isNotEmpty, () => Navigator.push(context, MaterialPageRoute( - builder: (_) => PrescriptionScreen(prescription: controller.prescription), + builder: (_) => PrescriptionScreen( + prescription: controller.medicines.join(", "), + ), )), ), ], @@ -193,26 +209,16 @@ class TranscriptionScreen extends StatelessWidget { ) { return SizedBox( width: double.infinity, - child: ElevatedButton( - onPressed: isEnabled ? onPressed : null, + child: ElevatedButton.icon( + icon: Icon(icon, color: Colors.deepPurple), + label: Text(title, style: const TextStyle(fontSize: 16, color: Colors.black87)), 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, + padding: const EdgeInsets.symmetric(vertical: 14), + backgroundColor: isEnabled ? Colors.white : Colors.white24, 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)), - ], - ), + onPressed: isEnabled ? onPressed : null, ), ); } -} \ No newline at end of file +} From ef025b826aa33dd8642c2eb3fad6a203300aa217 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 16:14:49 +0530 Subject: [PATCH 03/17] fix error --- .../transcription/data/deepgram_service.dart | 90 +++++++------------ .../domain/medical_insights.dart | 17 +++- .../transcription_controller.dart | 31 ++----- 3 files changed, 51 insertions(+), 87 deletions(-) diff --git a/lib/features/transcription/data/deepgram_service.dart b/lib/features/transcription/data/deepgram_service.dart index a6a89a5..baf7cbc 100644 --- a/lib/features/transcription/data/deepgram_service.dart +++ b/lib/features/transcription/data/deepgram_service.dart @@ -9,15 +9,11 @@ class DeepgramService { DeepgramService({String? apiKey}) : _apiKey = apiKey?.trim(); - // ============================= - // Resolve API Key - // ============================= String _resolveApiKey() { final configuredKey = _apiKey; if (configuredKey != null && configuredKey.isNotEmpty) { return configuredKey; } - try { return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim(); } catch (_) { @@ -25,9 +21,6 @@ class DeepgramService { } } - // ============================= - // Retry Logic (Production Grade) - // ============================= Future _retryPost({ required Uri uri, required Map headers, @@ -40,76 +33,46 @@ class DeepgramService { .post(uri, headers: headers, body: body) .timeout(const Duration(seconds: 30)); - if (response.statusCode == 200) { - return response; - } + if (response.statusCode == 200) return response; - // Retry only for server errors if (response.statusCode >= 500) { await Future.delayed(Duration(seconds: 2 * (attempt + 1))); continue; } else { - throw Exception( - 'Deepgram failed: ${response.statusCode} - ${response.body}'); + throw Exception('Deepgram failed: ${response.statusCode} - ${response.body}'); } } on TimeoutException { - if (attempt == retries - 1) { - throw Exception('Request timed out after multiple retries'); - } + if (attempt == retries - 1) throw Exception('Request timed out'); } catch (e) { - if (attempt == retries - 1) { - rethrow; - } + if (attempt == retries - 1) rethrow; } - - // Exponential backoff await Future.delayed(Duration(seconds: 2 * (attempt + 1))); } - throw Exception('Failed after $retries retries'); } - // ============================= - // Main Transcription Method - // ============================= Future transcribe(String recordingPath) async { final apiKey = _resolveApiKey(); - - if (apiKey.isEmpty) { - throw Exception('Missing DEEPGRAM_API_KEY in environment'); - } + 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', + final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&smart_format=true'); + + final response = await _retryPost( + uri: uri, + headers: { + 'Authorization': 'Token $apiKey', + 'Content-Type': 'application/octet-stream', + }, + body: bytes, ); - try { - final response = await _retryPost( - uri: uri, - headers: { - 'Authorization': 'Token $apiKey', - 'Content-Type': 'application/octet-stream', // 🔥 improved - }, - body: bytes, - ); - - return _parseTranscript(response.body); - } catch (e) { - throw Exception('Transcription error: $e'); - } + return _parseTranscript(response.body); } - // ============================= - // Response Parser (Robust) - // ============================= String _parseTranscript(String responseBody) { try { final decoded = json.decode(responseBody); @@ -118,15 +81,22 @@ class DeepgramService { return 'No speech detected'; } - final transcript = decoded['results']?['channels']?[0]?['alternatives']?[0]?['transcript']; - - if (transcript is String && transcript.trim().isNotEmpty) { - return transcript.trim(); + 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(); + } + } } return 'No speech detected'; - } catch (_) { - return 'Failed to parse transcription'; + } catch (e) { + throw Exception('Failed to parse Deepgram response: $e'); } } -} \ No newline at end of file +} diff --git a/lib/features/transcription/domain/medical_insights.dart b/lib/features/transcription/domain/medical_insights.dart index 8306bb5..38c7a90 100644 --- a/lib/features/transcription/domain/medical_insights.dart +++ b/lib/features/transcription/domain/medical_insights.dart @@ -11,12 +11,21 @@ class MedicalInsights { factory MedicalInsights.fromJson(Map json) { return MedicalInsights( - summary: json['summary'] ?? '', - symptoms: List.from(json['symptoms'] ?? []), - medicines: List.from(json['medicines'] ?? []), + summary: json['summary']?.toString() ?? '', + + symptoms: _parseList(json['symptoms']), + medicines: _parseList(json['medicines']), ); } + 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, @@ -24,4 +33,4 @@ class MedicalInsights { 'medicines': medicines, }; } -} \ 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 8e1fd02..73408cc 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -5,12 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; 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 } class TranscriptionController extends ChangeNotifier { @@ -20,36 +18,34 @@ class TranscriptionController extends ChangeNotifier { TranscriptionState state = TranscriptionState.idle; TranscriptionModel data = const TranscriptionModel(); + String? errorMessage; String _recordingPath = ''; - // Waveform 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; - - // ✅ NEW STRUCTURED GETTERS String get summary => data.insights?.summary ?? ''; + List get symptoms => data.insights?.symptoms ?? []; List get medicines => data.insights?.medicines ?? []; + @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; } @@ -68,11 +64,9 @@ class TranscriptionController extends ChangeNotifier { final granted = await requestPermissions(); if (!granted) return; } - final directory = await getTemporaryDirectory(); _recordingPath = '${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.m4a'; - await _audioRecorder.start( RecordConfig( encoder: AudioEncoder.aacLc, @@ -81,12 +75,10 @@ class TranscriptionController extends ChangeNotifier { ), path: _recordingPath, ); - data = const TranscriptionModel(); state = TranscriptionState.recording; _startWaveformAnimation(); notifyListeners(); - developer.log('Started recording to: $_recordingPath'); } catch (e) { _setError('Error starting recording: $e'); @@ -97,11 +89,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) { @@ -112,11 +102,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 { @@ -128,16 +116,13 @@ class TranscriptionController extends ChangeNotifier { } } - // ✅ UPDATED: Structured AI Processing Future _processWithGemini(String transcript) async { try { final MedicalInsights insights = await _geminiService.generateInsights(transcript); - data = data.copyWith(insights: insights); state = TranscriptionState.done; notifyListeners(); - developer.log('Gemini structured insights generated'); } catch (e) { _setError('Gemini error: $e'); @@ -165,11 +150,11 @@ class TranscriptionController extends ChangeNotifier { notifyListeners(); developer.log(message); } - + @override void dispose() { _waveformTimer?.cancel(); _audioRecorder.dispose(); super.dispose(); } -} \ No newline at end of file +} From cd9b7a3f8e2898fe9f83a051cf03128758ffbd9d Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 16:20:48 +0530 Subject: [PATCH 04/17] fix error --- lib/features/transcription/domain/medical_insights.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/features/transcription/domain/medical_insights.dart b/lib/features/transcription/domain/medical_insights.dart index 38c7a90..44984b9 100644 --- a/lib/features/transcription/domain/medical_insights.dart +++ b/lib/features/transcription/domain/medical_insights.dart @@ -11,13 +11,16 @@ class MedicalInsights { 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 From 8558d4ea6f3ed5e662762b9b8c11fe95fcc35d21 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 16:25:09 +0530 Subject: [PATCH 05/17] fix error --- .../transcription/presentation/transcription_controller.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_controller.dart b/lib/features/transcription/presentation/transcription_controller.dart index 73408cc..76cee88 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -9,6 +9,7 @@ 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 } class TranscriptionController extends ChangeNotifier { @@ -33,8 +34,8 @@ class TranscriptionController extends ChangeNotifier { String get transcription => data.rawTranscript; String get summary => data.insights?.summary ?? ''; - List get symptoms => data.insights?.symptoms ?? []; - List get medicines => data.insights?.medicines ?? []; + List get symptoms => List.unmodifiable(data.insights?.symptoms ?? []); + List get medicines => List.unmodifiable(data.insights?.medicines ?? []); @Deprecated('Use summary, symptoms, or medicines instead') String get prescription => summary; From b6f38e8e9f34b8323788d59b6f74b85fb8ccaca5 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 17:11:14 +0530 Subject: [PATCH 06/17] fix: resolve overflow and update UI for structured medical insights --- .../presentation/transcription_screen.dart | 203 ++++++++++++------ lib/screens/medical_insights_screen.dart | 59 +++++ 2 files changed, 195 insertions(+), 67 deletions(-) create mode 100644 lib/screens/medical_insights_screen.dart diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index 02d2a48..a2f6c17 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'; @@ -10,7 +10,6 @@ class TranscriptionScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // Listen to changes in the controller final controller = context.watch(); return Scaffold( @@ -34,19 +33,21 @@ class TranscriptionScreen extends StatelessWidget { const Text( 'DocPilot', style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), const SizedBox(height: 8), + Text( _statusText(controller.state), style: const TextStyle(fontSize: 16, color: Colors.white70), ), + const SizedBox(height: 30), - // Waveform Display + // 🎧 Waveform SizedBox( height: 100, child: Row( @@ -62,7 +63,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), ), @@ -71,38 +77,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 with Overflow Fix - Center( + // 🔥 Status Row (FIXED) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -110,7 +126,7 @@ 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 @@ -120,53 +136,90 @@ class TranscriptionScreen extends StatelessWidget { : Colors.amber, ), ), - // FIX: Wrapped in Expanded to prevent the 174px right overflow Expanded( child: Text( _statusDetailText(controller), textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.white + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, ), ), ), ], ), ), - const SizedBox(height: 40), - // Navigation buttons with Compilation Fix + const SizedBox(height: 20), + + // 🧠 Empty State + if (controller.state == TranscriptionState.done && + controller.transcription.isEmpty) + 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, children: [ _buildNavigationButton( - context, 'Transcription', Icons.record_voice_over, - controller.transcription.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => TranscriptionDetailScreen(transcription: controller.transcription), - )), + context, + 'Transcription', + Icons.record_voice_over, + controller.transcription.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => TranscriptionDetailScreen( + transcription: controller.transcription, + ), + ), + ), ), + const SizedBox(height: 16), + _buildNavigationButton( - context, 'Summary', Icons.summarize, - controller.summary.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => SummaryScreen(summary: controller.summary), - )), + context, + 'Summary', + Icons.summarize, + controller.summary.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => SummaryScreen( + summary: controller.summary, + ), + ), + ), ), + const SizedBox(height: 16), - // FIX: Using controller.medicines instead of controller.prescription + + // ✅ NEW: Medical Insights Screen _buildNavigationButton( - context, 'Prescription', Icons.medication, - controller.medicines.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => PrescriptionScreen( - prescription: controller.medicines.join(", "), + context, + 'Medical Insights', + Icons.medical_services, + controller.medicines.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MedicalInsightsScreen( + symptoms: controller.symptoms, + medicines: controller.medicines, + ), ), - )), + ), ), ], ), @@ -181,22 +234,33 @@ 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'; + case TranscriptionState.recording: + return 'Recording your voice...'; + case TranscriptionState.transcribing: + return 'Transcribing your voice...'; + case TranscriptionState.processing: + return 'Processing with AI...'; + 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'; + case TranscriptionState.recording: + return 'Recording in progress'; + case TranscriptionState.transcribing: + return 'Processing audio...'; + case TranscriptionState.processing: + return 'Generating insights...'; + case TranscriptionState.done: + return 'Ready to view results'; + case TranscriptionState.error: + return controller.errorMessage ?? 'Error occurred'; + default: + return 'Press the microphone button to start'; } } @@ -211,14 +275,19 @@ class TranscriptionScreen extends StatelessWidget { width: double.infinity, child: ElevatedButton.icon( icon: Icon(icon, color: Colors.deepPurple), - label: Text(title, style: const TextStyle(fontSize: 16, color: Colors.black87)), + label: Text( + title, + style: const TextStyle(fontSize: 16, color: Colors.black87), + ), style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), + padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: isEnabled ? Colors.white : Colors.white24, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), ), onPressed: isEnabled ? onPressed : null, ), ); } -} +} \ No newline at end of file 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 From 1e160e1cc3511abe095ce028b15ed97c1e761685 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 17:34:14 +0530 Subject: [PATCH 07/17] fix issue --- .../transcription_controller.dart | 6 + .../presentation/transcription_screen.dart | 107 +++++++++--------- lib/main.dart | 21 ++-- pubspec.yaml | 3 +- 4 files changed, 72 insertions(+), 65 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_controller.dart b/lib/features/transcription/presentation/transcription_controller.dart index 76cee88..33b414a 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -151,6 +151,12 @@ class TranscriptionController extends ChangeNotifier { notifyListeners(); 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() { diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index a2f6c17..6c9934e 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -47,7 +47,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 30), - // 🎧 Waveform + // Waveform SizedBox( height: 100, child: Row( @@ -80,7 +80,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 40), - // 🎤 Mic Button + // Mic Button Center( child: GestureDetector( onTap: controller.isProcessing ? null : controller.toggleRecording, @@ -116,7 +116,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // 🔥 Status Row (FIXED) + // Status Row (FIXED) Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( @@ -167,63 +167,58 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), // 🚀 Navigation Buttons - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildNavigationButton( - context, - 'Transcription', - Icons.record_voice_over, - controller.transcription.isNotEmpty && !controller.isProcessing, - () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => TranscriptionDetailScreen( - transcription: controller.transcription, - ), - ), - ), - ), +Expanded( + child: ListView( + children: [ + // 1. Summary Button + _buildNavigationButton( + context, + 'Summary', + Icons.description, + controller.summary.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => SummaryScreen(summary: controller.summary)) + ), + ), + + const SizedBox(height: 12), - const SizedBox(height: 16), + // 2. Prescription Button (Legacy Adapter) + _buildNavigationButton( + context, + 'Prescription', + Icons.medication, + controller.prescription.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => PrescriptionScreen(prescription: controller.prescription)), + ), + ), - _buildNavigationButton( - context, - 'Summary', - Icons.summarize, - controller.summary.isNotEmpty && !controller.isProcessing, - () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => SummaryScreen( - summary: controller.summary, - ), - ), - ), - ), + const SizedBox(height: 12), - const SizedBox(height: 16), + // 3. Medical Insights Button (FIXED LOGIC) + _buildNavigationButton( + context, + 'Medical Insights', + Icons.medical_services, + // ✅ Enable if EITHER symptoms or medicines are found + (controller.symptoms.isNotEmpty || controller.medicines.isNotEmpty) && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MedicalInsightsScreen( + symptoms: controller.symptoms, + medicines: controller.medicines, + ), + ), + ), + ), + ], + ), +), - // ✅ NEW: Medical Insights Screen - _buildNavigationButton( - context, - 'Medical Insights', - Icons.medical_services, - controller.medicines.isNotEmpty && !controller.isProcessing, - () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => MedicalInsightsScreen( - symptoms: controller.symptoms, - medicines: controller.medicines, - ), - ), - ), - ), - ], - ), - ), ], ), ), diff --git a/lib/main.dart b/lib/main.dart index 48544bc..bf7a6d4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,30 +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 { 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("Warning: Could not load .env file: $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(const MyApp()); + 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/pubspec.yaml b/pubspec.yaml index 6bfa8df..52c287a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,8 +64,7 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - assets: - - .env + # assets: # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see From 8584cbc095b46e7ab87b8d23063d80c93b495f69 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 18:58:43 +0530 Subject: [PATCH 08/17] fix transciption_screen file --- .../presentation/transcription_screen.dart | 194 ++++++++++-------- 1 file changed, 103 insertions(+), 91 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index 6c9934e..f645670 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -116,7 +116,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // Status Row (FIXED) + // Status Row Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( @@ -154,9 +154,10 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // 🧠 Empty State + // Empty State if (controller.state == TranscriptionState.done && - controller.transcription.isEmpty) + (controller.transcription.isEmpty || + controller.transcription == "No speech detected.")) const Center( child: Text( "No speech detected. Try again.", @@ -166,67 +167,112 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // 🚀 Navigation Buttons -Expanded( - child: ListView( - children: [ - // 1. Summary Button - _buildNavigationButton( - context, - 'Summary', - Icons.description, - controller.summary.isNotEmpty && !controller.isProcessing, - () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => SummaryScreen(summary: controller.summary)) - ), - ), - - const SizedBox(height: 12), + // Navigation Buttons + Expanded( + child: ListView( + children: [ + _buildNavigationButton( + context, + 'Summary', + Icons.description, + controller.summary.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => SummaryScreen(summary: controller.summary)) + ), + ), + + const SizedBox(height: 12), - // 2. Prescription Button (Legacy Adapter) - _buildNavigationButton( - context, - 'Prescription', - Icons.medication, - controller.prescription.isNotEmpty && !controller.isProcessing, - () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => PrescriptionScreen(prescription: controller.prescription)), - ), - ), + _buildNavigationButton( + 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: 12), + const SizedBox(height: 12), - // 3. Medical Insights Button (FIXED LOGIC) - _buildNavigationButton( - context, - 'Medical Insights', - Icons.medical_services, - // ✅ Enable if EITHER symptoms or medicines are found - (controller.symptoms.isNotEmpty || controller.medicines.isNotEmpty) && !controller.isProcessing, - () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => MedicalInsightsScreen( - symptoms: controller.symptoms, - medicines: controller.medicines, + _buildNavigationButton( + context, + 'Medical Insights', + Icons.analytics, + controller.summary.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MedicalInsightsScreen(summary: controller.summary), + ), + ), + ), + ], + ), + ), + ], ), ), ), ), - ], - ), -), + ); + } - ], + Widget _buildNavigationButton( + BuildContext context, + String label, + IconData icon, + bool enabled, + VoidCallback onTap, + ) { + 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, + ), + ], + ), ), ), ); } + // FIXED: Explicitly handles .done and removed duplicate code String _statusText(TranscriptionState state) { switch (state) { case TranscriptionState.recording: @@ -235,6 +281,8 @@ Expanded( return 'Transcribing your voice...'; case TranscriptionState.processing: return 'Processing with AI...'; + case TranscriptionState.done: + return 'Analysis ready'; case TranscriptionState.error: return 'Something went wrong'; default: @@ -243,46 +291,10 @@ Expanded( } 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 insights...'; - 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, - IconData icon, - bool isEnabled, - VoidCallback onPressed, - ) { - return SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: Icon(icon, color: Colors.deepPurple), - label: Text( - title, - style: const TextStyle(fontSize: 16, color: Colors.black87), - ), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: isEnabled ? Colors.white : Colors.white24, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - onPressed: isEnabled ? onPressed : null, - ), - ); + if (controller.isRecording) return 'Recording in progress...'; + if (controller.state == TranscriptionState.processing) return 'Converting speech to text...'; + if (controller.state == TranscriptionState.summarizing) return 'Analyzing medical content...'; + if (controller.state == TranscriptionState.done) return 'Review your insights below'; + return 'Tap the microphone to begin'; } -} \ No newline at end of file +} From a36342500f84255cbf30cf45a25252e18048b2b7 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 19:04:52 +0530 Subject: [PATCH 09/17] fix transciption_screen file --- .../presentation/transcription_screen.dart | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index f645670..f431213 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -39,15 +39,13 @@ class TranscriptionScreen extends StatelessWidget { ), ), const SizedBox(height: 8), - Text( _statusText(controller.state), style: const TextStyle(fontSize: 16, color: Colors.white70), ), - const SizedBox(height: 30), - // Waveform + // Waveform SizedBox( height: 100, child: Row( @@ -80,7 +78,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 40), - // Mic Button + // Mic Button Center( child: GestureDetector( onTap: controller.isProcessing ? null : controller.toggleRecording, @@ -116,7 +114,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // Status Row + // Status Row Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( @@ -154,7 +152,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // Empty State + // Empty State if (controller.state == TranscriptionState.done && (controller.transcription.isEmpty || controller.transcription == "No speech detected.")) @@ -167,7 +165,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // Navigation Buttons + // Navigation Buttons Expanded( child: ListView( children: [ @@ -203,6 +201,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 12), + // FIXED: Passing correct List parameters to MedicalInsightsScreen _buildNavigationButton( context, 'Medical Insights', @@ -211,7 +210,10 @@ class TranscriptionScreen extends StatelessWidget { () => Navigator.push( context, MaterialPageRoute( - builder: (_) => MedicalInsightsScreen(summary: controller.summary), + builder: (_) => MedicalInsightsScreen( + symptoms: controller.symptoms, + medicines: controller.medicines, + ), ), ), ), @@ -272,15 +274,14 @@ class TranscriptionScreen extends StatelessWidget { ); } - // FIXED: Explicitly handles .done and removed duplicate code String _statusText(TranscriptionState state) { switch (state) { case TranscriptionState.recording: return 'Recording your voice...'; case TranscriptionState.transcribing: - return 'Transcribing your voice...'; + return 'Transcribing...'; case TranscriptionState.processing: - return 'Processing with AI...'; + return 'Analyzing with AI...'; case TranscriptionState.done: return 'Analysis ready'; case TranscriptionState.error: @@ -292,8 +293,9 @@ class TranscriptionScreen extends StatelessWidget { String _statusDetailText(TranscriptionController controller) { if (controller.isRecording) return 'Recording in progress...'; - if (controller.state == TranscriptionState.processing) return 'Converting speech to text...'; - if (controller.state == TranscriptionState.summarizing) return 'Analyzing medical content...'; + // FIXED: Adjusted to use existing enum states (transcribing -> processing) + 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'; } From 64da1f2fb8ed63e8dd2322740a9fb2f41418cbbe Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 19:12:38 +0530 Subject: [PATCH 10/17] fix transciption_screen file --- .../transcription/presentation/transcription_screen.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index f431213..fcc8564 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -152,10 +152,10 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // Empty State + // Empty State (FIXED: Removed period to match controller sentinel) if (controller.state == TranscriptionState.done && (controller.transcription.isEmpty || - controller.transcription == "No speech detected.")) + controller.transcription == "No speech detected")) const Center( child: Text( "No speech detected. Try again.", @@ -187,7 +187,7 @@ class TranscriptionScreen extends StatelessWidget { 'Full Transcription', Icons.text_snippet, controller.transcription.isNotEmpty && - controller.transcription != "No speech detected." && + controller.transcription != "No speech detected" && !controller.isProcessing, () => Navigator.push( context, @@ -201,7 +201,6 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 12), - // FIXED: Passing correct List parameters to MedicalInsightsScreen _buildNavigationButton( context, 'Medical Insights', @@ -274,6 +273,7 @@ class TranscriptionScreen extends StatelessWidget { ); } + // FIXED: Explicitly handles .done and removed duplicate switch cases String _statusText(TranscriptionState state) { switch (state) { case TranscriptionState.recording: @@ -293,7 +293,6 @@ class TranscriptionScreen extends StatelessWidget { String _statusDetailText(TranscriptionController controller) { if (controller.isRecording) return 'Recording in progress...'; - // FIXED: Adjusted to use existing enum states (transcribing -> processing) 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'; From 906b7c3aecb0c545dd015332e7654a7826df1ca7 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 19:17:15 +0530 Subject: [PATCH 11/17] fix transciption_screen file --- .../transcription/presentation/transcription_screen.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index fcc8564..18c1f2e 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -152,7 +152,7 @@ class TranscriptionScreen extends StatelessWidget { const SizedBox(height: 20), - // Empty State (FIXED: Removed period to match controller sentinel) + // Empty State (FIXED: No period in sentinel string) if (controller.state == TranscriptionState.done && (controller.transcription.isEmpty || controller.transcription == "No speech detected")) @@ -273,7 +273,6 @@ class TranscriptionScreen extends StatelessWidget { ); } - // FIXED: Explicitly handles .done and removed duplicate switch cases String _statusText(TranscriptionState state) { switch (state) { case TranscriptionState.recording: @@ -281,7 +280,7 @@ class TranscriptionScreen extends StatelessWidget { case TranscriptionState.transcribing: return 'Transcribing...'; case TranscriptionState.processing: - return 'Analyzing with AI...'; + return 'Processing with Gemini...'; case TranscriptionState.done: return 'Analysis ready'; case TranscriptionState.error: From 8795c59a27cf8801f3b33bc366ed28c238b83e1a Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 19:21:25 +0530 Subject: [PATCH 12/17] fix transciption_screen file --- .../transcription/presentation/transcription_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index 18c1f2e..0c33565 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -278,11 +278,11 @@ class TranscriptionScreen extends StatelessWidget { case TranscriptionState.recording: return 'Recording your voice...'; case TranscriptionState.transcribing: - return 'Transcribing...'; + return 'Transcribing your voice...'; case TranscriptionState.processing: return 'Processing with Gemini...'; case TranscriptionState.done: - return 'Analysis ready'; + return 'Transcription complete!'; case TranscriptionState.error: return 'Something went wrong'; default: From 56050f6cdb67b25803fd48bc8ceebc6593e64359 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 20:14:05 +0530 Subject: [PATCH 13/17] feat: add persistent transcription history with structured insights --- .../data/local_storage_service.dart | 26 ++++++++ .../domain/transcription_history_model.dart | 33 ++++++++++ .../presentation/history_screen.dart | 61 +++++++++++++++++++ .../transcription_controller.dart | 14 +++++ 4 files changed, 134 insertions(+) create mode 100644 lib/features/transcription/data/local_storage_service.dart create mode 100644 lib/features/transcription/domain/transcription_history_model.dart create mode 100644 lib/features/transcription/presentation/history_screen.dart diff --git a/lib/features/transcription/data/local_storage_service.dart b/lib/features/transcription/data/local_storage_service.dart new file mode 100644 index 0000000..73d98c8 --- /dev/null +++ b/lib/features/transcription/data/local_storage_service.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../domain/transcription_history_model.dart'; + +class LocalStorageService { + static const String key = "transcription_history"; + + Future save(TranscriptionHistoryModel item) async { + final prefs = await SharedPreferences.getInstance(); + final list = prefs.getStringList(key) ?? []; + + list.add(jsonEncode(item.toJson())); + await prefs.setStringList(key, list); + } + + Future> getAll() async { + final prefs = await SharedPreferences.getInstance(); + final list = prefs.getStringList(key) ?? []; + + return list + .map((e) => TranscriptionHistoryModel.fromJson(jsonDecode(e))) + .toList() + .reversed + .toList(); + } +} \ No newline at end of file diff --git a/lib/features/transcription/domain/transcription_history_model.dart b/lib/features/transcription/domain/transcription_history_model.dart new file mode 100644 index 0000000..1f26940 --- /dev/null +++ b/lib/features/transcription/domain/transcription_history_model.dart @@ -0,0 +1,33 @@ +class TranscriptionHistoryModel { + final String transcript; + final String summary; + final List symptoms; + final List medicines; + final DateTime createdAt; + + TranscriptionHistoryModel({ + required this.transcript, + required this.summary, + required this.symptoms, + required this.medicines, + required this.createdAt, + }); + + Map toJson() => { + 'transcript': transcript, + 'summary': summary, + 'symptoms': symptoms, + 'medicines': medicines, + 'createdAt': createdAt.toIso8601String(), + }; + + factory TranscriptionHistoryModel.fromJson(Map json) { + return TranscriptionHistoryModel( + transcript: json['transcript'], + summary: json['summary'], + symptoms: List.from(json['symptoms']), + medicines: List.from(json['medicines']), + createdAt: DateTime.parse(json['createdAt']), + ); + } +} \ No newline at end of file diff --git a/lib/features/transcription/presentation/history_screen.dart b/lib/features/transcription/presentation/history_screen.dart new file mode 100644 index 0000000..6ebe1a6 --- /dev/null +++ b/lib/features/transcription/presentation/history_screen.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import '../data/local_storage_service.dart'; +import '../domain/transcription_history_model.dart'; +import 'package:doc_pilot_new_app_gradel_fix/screens/medical_insights_screen.dart'; + +class HistoryScreen extends StatefulWidget { + const HistoryScreen({super.key}); + + @override + State createState() => _HistoryScreenState(); +} + +class _HistoryScreenState extends State { + List history = []; + + @override + void initState() { + super.initState(); + loadHistory(); + } + + Future loadHistory() async { + history = await LocalStorageService().getAll(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("History")), + body: history.isEmpty + ? const Center(child: Text("No history yet")) + : ListView.builder( + itemCount: history.length, + itemBuilder: (context, index) { + final item = history[index]; + + return ListTile( + title: Text( + item.summary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(item.createdAt.toString()), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MedicalInsightsScreen( + symptoms: item.symptoms, + medicines: item.medicines, + ), + ), + ); + }, + ); + }, + ), + ); + } +} \ 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 33b414a..6454885 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -6,9 +6,11 @@ import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; import 'package:permission_handler/permission_handler.dart'; import '../data/deepgram_service.dart'; +import '../data/local_storage_service.dart'; import '../data/gemini_service.dart'; import '../domain/transcription_model.dart'; import '../domain/medical_insights.dart'; +import '../domain/transcription_history_model.dart'; enum TranscriptionState { idle, recording, transcribing, processing, done, error } @@ -16,6 +18,7 @@ class TranscriptionController extends ChangeNotifier { final _audioRecorder = AudioRecorder(); final _deepgramService = DeepgramService(); final _geminiService = GeminiService(); + final _localStorageService = LocalStorageService(); TranscriptionState state = TranscriptionState.idle; TranscriptionModel data = const TranscriptionModel(); @@ -122,6 +125,17 @@ class TranscriptionController extends ChangeNotifier { final MedicalInsights insights = await _geminiService.generateInsights(transcript); data = data.copyWith(insights: insights); + + final historyItem = TranscriptionHistoryModel( + transcript: transcript, + summary: insights.summary, + symptoms: insights.symptoms, + medicines: insights.medicines, + createdAt: DateTime.now(), + ); + + await _localStorageService.save(historyItem); + state = TranscriptionState.done; notifyListeners(); developer.log('Gemini structured insights generated'); From b5eab34f2d0478a76393996d68c797cf888fb722 Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 20:41:20 +0530 Subject: [PATCH 14/17] feat: add persistent transcription history with structured insights --- .../transcription/data/deepgram_service.dart | 46 +++--- .../data/local_storage_service.dart | 59 ++++++-- .../domain/transcription_model.dart | 47 ++++-- .../presentation/history_screen.dart | 96 ++++++++---- .../transcription_controller.dart | 139 ++++++------------ 5 files changed, 206 insertions(+), 181 deletions(-) diff --git a/lib/features/transcription/data/deepgram_service.dart b/lib/features/transcription/data/deepgram_service.dart index baf7cbc..cdacad2 100644 --- a/lib/features/transcription/data/deepgram_service.dart +++ b/lib/features/transcription/data/deepgram_service.dart @@ -11,14 +11,8 @@ class DeepgramService { String _resolveApiKey() { final configuredKey = _apiKey; - if (configuredKey != null && configuredKey.isNotEmpty) { - return configuredKey; - } - try { - return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim(); - } catch (_) { - return ''; - } + if (configuredKey != null && configuredKey.isNotEmpty) return configuredKey; + return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim(); } Future _retryPost({ @@ -27,6 +21,8 @@ class DeepgramService { required List body, int retries = 3, }) async { + http.Response? lastResponse; + for (int attempt = 0; attempt < retries; attempt++) { try { final response = await http @@ -35,20 +31,29 @@ class DeepgramService { if (response.statusCode == 200) return response; + lastResponse = response; + if (response.statusCode >= 500) { await Future.delayed(Duration(seconds: 2 * (attempt + 1))); continue; } else { - throw Exception('Deepgram failed: ${response.statusCode} - ${response.body}'); + throw Exception('Deepgram error (${response.statusCode}): ${response.body}'); } } on TimeoutException { - if (attempt == retries - 1) throw Exception('Request timed out'); + if (attempt == retries - 1) throw Exception('Deepgram request timed out'); + } on Exception { + rethrow; } catch (e) { if (attempt == retries - 1) rethrow; } + await Future.delayed(Duration(seconds: 2 * (attempt + 1))); } - throw Exception('Failed after $retries retries'); + + throw Exception( + 'Deepgram failed after $retries attempts. ' + 'Last status: ${lastResponse?.statusCode ?? "Unknown"}' + ); } Future transcribe(String recordingPath) async { @@ -76,24 +81,11 @@ class DeepgramService { String _parseTranscript(String responseBody) { try { final decoded = json.decode(responseBody); - - if (decoded is! Map) { - return 'No speech detected'; - } - - final results = decoded['results']; - final channels = results?['channels']; + final transcript = decoded['results']?['channels']?[0]?['alternatives']?[0]?['transcript']; - 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(); - } - } + if (transcript is String && transcript.trim().isNotEmpty) { + return transcript.trim(); } - return 'No speech detected'; } catch (e) { throw Exception('Failed to parse Deepgram response: $e'); diff --git a/lib/features/transcription/data/local_storage_service.dart b/lib/features/transcription/data/local_storage_service.dart index 73d98c8..0d9986d 100644 --- a/lib/features/transcription/data/local_storage_service.dart +++ b/lib/features/transcription/data/local_storage_service.dart @@ -1,26 +1,55 @@ import 'dart:convert'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../domain/transcription_history_model.dart'; class LocalStorageService { - static const String key = "transcription_history"; + static const String _key = "transcription_history_secure"; + + // Encrypted storage instance + final _storage = const FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), + ); + /// Saves a history item securely by encrypting the JSON string Future save(TranscriptionHistoryModel item) async { - final prefs = await SharedPreferences.getInstance(); - final list = prefs.getStringList(key) ?? []; - - list.add(jsonEncode(item.toJson())); - await prefs.setStringList(key, list); + try { + final List currentHistory = await getAll(); + currentHistory.add(item); + + final String encodedData = jsonEncode( + currentHistory.map((e) => e.toJson()).toList(), + ); + + await _storage.write(key: _key, value: encodedData); + } catch (e) { + throw Exception("Failed to secure medical data: $e"); + } } + /// Retrieves and decrypts all transcription history Future> getAll() async { - final prefs = await SharedPreferences.getInstance(); - final list = prefs.getStringList(key) ?? []; + try { + final String? securedJson = await _storage.read(key: _key); + + if (securedJson == null || securedJson.isEmpty) { + return []; + } + + final List decodedList = jsonDecode(securedJson); + + return decodedList + .map((item) => TranscriptionHistoryModel.fromJson(item)) + .toList() + .reversed // Newest first + .toList(); + } catch (e) { + return []; + } + } - return list - .map((e) => TranscriptionHistoryModel.fromJson(jsonDecode(e))) - .toList() - .reversed - .toList(); + /// Optional: Clear all history for privacy compliance + Future clearAll() async { + await _storage.delete(key: _key); } -} \ No newline at end of file +} diff --git a/lib/features/transcription/domain/transcription_model.dart b/lib/features/transcription/domain/transcription_model.dart index 980ce65..2b64cae 100644 --- a/lib/features/transcription/domain/transcription_model.dart +++ b/lib/features/transcription/domain/transcription_model.dart @@ -1,21 +1,40 @@ import 'medical_insights.dart'; -class TranscriptionModel { - final String rawTranscript; - final MedicalInsights? insights; +class TranscriptionHistoryModel { + final String transcript; + final String summary; + final List symptoms; + final List medicines; + final DateTime createdAt; - const TranscriptionModel({ - this.rawTranscript = '', - this.insights, + const TranscriptionHistoryModel({ + required this.transcript, + required this.summary, + required this.symptoms, + required this.medicines, + required this.createdAt, }); - TranscriptionModel copyWith({ - String? rawTranscript, - MedicalInsights? insights, - }) { - return TranscriptionModel( - rawTranscript: rawTranscript ?? this.rawTranscript, - insights: insights ?? this.insights, + factory TranscriptionHistoryModel.fromJson(Map json) { + return TranscriptionHistoryModel( + transcript: json['transcript'] ?? '', + summary: json['summary'] ?? '', + // FIXED: Use null-coalescing and casting to prevent crashes on missing lists + symptoms: (json['symptoms'] as List?)?.map((e) => e.toString()).toList() ?? [], + medicines: (json['medicines'] as List?)?.map((e) => e.toString()).toList() ?? [], + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), ); } -} \ No newline at end of file + + Map toJson() { + return { + 'transcript': transcript, + 'summary': summary, + 'symptoms': symptoms, + 'medicines': medicines, + 'createdAt': createdAt.toIso8601String(), + }; + } +} diff --git a/lib/features/transcription/presentation/history_screen.dart b/lib/features/transcription/presentation/history_screen.dart index 6ebe1a6..9ed260b 100644 --- a/lib/features/transcription/presentation/history_screen.dart +++ b/lib/features/transcription/presentation/history_screen.dart @@ -12,6 +12,7 @@ class HistoryScreen extends StatefulWidget { class _HistoryScreenState extends State { List history = []; + bool isLoading = true; @override void initState() { @@ -20,42 +21,73 @@ class _HistoryScreenState extends State { } Future loadHistory() async { - history = await LocalStorageService().getAll(); - setState(() {}); + final results = await LocalStorageService().getAll(); + + // FIXED: Guard against calling setState if the widget is no longer in the tree + if (!mounted) return; + + setState(() { + history = results; + isLoading = false; + }); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("History")), - body: history.isEmpty - ? const Center(child: Text("No history yet")) - : ListView.builder( - itemCount: history.length, - itemBuilder: (context, index) { - final item = history[index]; - - return ListTile( - title: Text( - item.summary, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(item.createdAt.toString()), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => MedicalInsightsScreen( - symptoms: item.symptoms, - medicines: item.medicines, - ), - ), - ); - }, - ); - }, - ), + appBar: AppBar(title: const Text("Transcription History")), + body: _buildBody(), + ); + } + + Widget _buildBody() { + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (history.isEmpty) { + return const Center(child: Text("No history found")); + } + + return ListView.builder( + itemCount: history.length, + padding: const EdgeInsets.symmetric(vertical: 8), + itemBuilder: (context, index) { + final item = history[index]; + + return ListTile( + leading: const Icon(Icons.history_medical, color: Colors.blue), + title: Text( + item.summary.isNotEmpty ? item.summary : "No Summary Available", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + subtitle: Text( + _formatDate(item.createdAt), + style: TextStyle(color: Colors.grey[600], fontSize: 12), + ), + trailing: const Icon(Icons.chevron_right), + onTap: () { + // FIXED: Passing full data so the insights screen isn't incomplete + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MedicalInsightsScreen( + symptoms: item.symptoms, + medicines: item.medicines, + summary: item.summary, // Added summary + transcript: item.transcript, // Added transcript if supported + ), + ), + ); + }, + ); + }, ); } -} \ No newline at end of file + + String _formatDate(DateTime date) { + return "${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}"; + } +} diff --git a/lib/features/transcription/presentation/transcription_controller.dart b/lib/features/transcription/presentation/transcription_controller.dart index 6454885..e82c943 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -1,10 +1,10 @@ import 'dart:developer' as developer; -import 'dart:math'; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; import 'package:permission_handler/permission_handler.dart'; + import '../data/deepgram_service.dart'; import '../data/local_storage_service.dart'; import '../data/gemini_service.dart'; @@ -18,6 +18,7 @@ class TranscriptionController extends ChangeNotifier { final _audioRecorder = AudioRecorder(); final _deepgramService = DeepgramService(); final _geminiService = GeminiService(); + final _localStorageService = LocalStorageService(); TranscriptionState state = TranscriptionState.idle; @@ -25,10 +26,9 @@ class TranscriptionController extends ChangeNotifier { String? errorMessage; String _recordingPath = ''; - final List waveformValues = List.filled(40, 0.0); - Timer? _waveformTimer; + bool get isRecording => state == TranscriptionState.recording; bool get isProcessing => state == TranscriptionState.transcribing || @@ -36,96 +36,66 @@ class TranscriptionController extends ChangeNotifier { String get transcription => data.rawTranscript; String get summary => data.insights?.summary ?? ''; - + String get prescription => data.prescription ?? ''; // FIXED: Return actual prescription + List get symptoms => List.unmodifiable(data.insights?.symptoms ?? []); List get medicines => List.unmodifiable(data.insights?.medicines ?? []); - @Deprecated('Use summary, symptoms, or medicines instead') - String get prescription => summary; + bool get hasInsights => symptoms.isNotEmpty || medicines.isNotEmpty || summary.isNotEmpty; 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'); + + _setError(status.isPermanentlyDenied + ? 'Microphone permission permanently denied. Please enable it in settings.' + : 'Microphone permission denied'); return false; } - Future toggleRecording() async { - if (isRecording) { - await _stopRecording(); - } else { - await _startRecording(); - } - } - - Future _startRecording() async { - try { - if (!await _audioRecorder.hasPermission()) { - final granted = await requestPermissions(); - if (!granted) return; - } - final directory = await getTemporaryDirectory(); - _recordingPath = - '${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.m4a'; - await _audioRecorder.start( - RecordConfig( - encoder: AudioEncoder.aacLc, - bitRate: 128000, - sampleRate: 44100, - ), - path: _recordingPath, - ); - data = const TranscriptionModel(); - state = TranscriptionState.recording; - _startWaveformAnimation(); + Future _processWithGemini(String transcript) async { + // 1. Handle Empty Input Early + if (transcript.trim().isEmpty || transcript == "No speech detected") { + state = TranscriptionState.idle; notifyListeners(); - developer.log('Started recording to: $_recordingPath'); - } catch (e) { - _setError('Error starting recording: $e'); + return; } - } - Future _stopRecording() async { try { - _waveformTimer?.cancel(); - _resetWaveform(); - await _audioRecorder.stop(); - state = TranscriptionState.transcribing; + state = TranscriptionState.processing; notifyListeners(); - developer.log('Recording stopped, transcribing...'); - await _transcribe(); - } catch (e) { - _setError('Error stopping recording: $e'); - } - } - Future _transcribe() async { - try { - final transcript = await _deepgramService.transcribe(_recordingPath); - data = data.copyWith(rawTranscript: transcript); - state = TranscriptionState.processing; + // Parallelize AI calls for better performance + final results = await Future.wait([ + _geminiService.generateSummary(transcript), + _geminiService.generatePrescription(transcript), + _geminiService.generateInsights(transcript), + ]); + + final String summaryText = results[0] as String; + final String prescriptionText = results[1] as String; + final MedicalInsights insights = results[2] as MedicalInsights; + + data = data.copyWith( + insights: insights, + summary: summaryText, + prescription: prescriptionText, + ); + + // 2. Mark UI Done BEFORE persistence to ensure responsiveness + state = TranscriptionState.done; notifyListeners(); - if (transcript.isNotEmpty && transcript != 'No speech detected') { - await _processWithGemini(transcript); - } else { - state = TranscriptionState.done; - notifyListeners(); - } + + // 3. Isolated Persistence (Doesn't break UI on failure) + _persistHistory(transcript, insights); + } catch (e) { - _setError('Transcription error: $e'); + _setError('Gemini error: $e'); } } - Future _processWithGemini(String transcript) async { + Future _persistHistory(String transcript, MedicalInsights insights) async { try { - final MedicalInsights insights = - await _geminiService.generateInsights(transcript); - data = data.copyWith(insights: insights); - final historyItem = TranscriptionHistoryModel( transcript: transcript, summary: insights.summary, @@ -133,29 +103,10 @@ class TranscriptionController extends ChangeNotifier { medicines: insights.medicines, createdAt: DateTime.now(), ); - await _localStorageService.save(historyItem); - - state = TranscriptionState.done; - notifyListeners(); - developer.log('Gemini structured insights generated'); + developer.log('History persisted successfully'); } catch (e) { - _setError('Gemini error: $e'); - } - } - - void _startWaveformAnimation() { - _waveformTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { - for (int i = 0; i < waveformValues.length; i++) { - waveformValues[i] = Random().nextDouble(); - } - notifyListeners(); - }); - } - - void _resetWaveform() { - for (int i = 0; i < waveformValues.length; i++) { - waveformValues[i] = 0.0; + developer.log('Persistence failed: $e', level: 1000); } } @@ -163,7 +114,7 @@ class TranscriptionController extends ChangeNotifier { errorMessage = message; state = TranscriptionState.error; notifyListeners(); - developer.log(message); + developer.log(message, name: 'TranscriptionController', error: message); } void checkConfigStatus(bool isLoaded) { @@ -179,3 +130,5 @@ class TranscriptionController extends ChangeNotifier { super.dispose(); } } + + From 916c5664a58c9a65d0a55db649a72831edd9db7b Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 21:00:37 +0530 Subject: [PATCH 15/17] feat: add persistent transcription history with structured insights --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 2 +- .../data/local_storage_service.dart | 56 +++------ .../domain/transcription_history_model.dart | 50 ++++---- .../transcription_controller.dart | 114 ++++++++---------- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 56 +++++++++ pubspec.yaml | 1 + 10 files changed, 152 insertions(+), 136 deletions(-) diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index aa49780..3c85cfe 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index a42444d..10fd726 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.1" apply false + id "com.android.application" version "8.6.0" apply false id "org.jetbrains.kotlin.android" version "1.8.22" apply false } diff --git a/lib/features/transcription/data/local_storage_service.dart b/lib/features/transcription/data/local_storage_service.dart index 0d9986d..256ee59 100644 --- a/lib/features/transcription/data/local_storage_service.dart +++ b/lib/features/transcription/data/local_storage_service.dart @@ -4,52 +4,26 @@ import '../domain/transcription_history_model.dart'; class LocalStorageService { static const String _key = "transcription_history_secure"; - - // Encrypted storage instance - final _storage = const FlutterSecureStorage( - aOptions: AndroidOptions(encryptedSharedPreferences: true), - iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), - ); + final _storage = const FlutterSecureStorage(); - /// Saves a history item securely by encrypting the JSON string - Future save(TranscriptionHistoryModel item) async { + // Internal helper to get RAW list without reversing (prevents corruption) + Future> _getRawList() async { try { - final List currentHistory = await getAll(); - currentHistory.add(item); - - final String encodedData = jsonEncode( - currentHistory.map((e) => e.toJson()).toList(), - ); - - await _storage.write(key: _key, value: encodedData); - } catch (e) { - throw Exception("Failed to secure medical data: $e"); - } + final jsonStr = await _storage.read(key: _key); + if (jsonStr == null) return []; + final List decoded = jsonDecode(jsonStr); + return decoded.map((item) => TranscriptionHistoryModel.fromJson(item)).toList(); + } catch (_) { return []; } } - /// Retrieves and decrypts all transcription history - Future> getAll() async { - try { - final String? securedJson = await _storage.read(key: _key); - - if (securedJson == null || securedJson.isEmpty) { - return []; - } - - final List decodedList = jsonDecode(securedJson); - - return decodedList - .map((item) => TranscriptionHistoryModel.fromJson(item)) - .toList() - .reversed // Newest first - .toList(); - } catch (e) { - return []; - } + Future save(TranscriptionHistoryModel item) async { + final list = await _getRawList(); // Get chronological order + list.add(item); + await _storage.write(key: _key, value: jsonEncode(list.map((e) => e.toJson()).toList())); } - /// Optional: Clear all history for privacy compliance - Future clearAll() async { - await _storage.delete(key: _key); + Future> getAll() async { + final list = await _getRawList(); + return list.reversed.toList(); // Reverse ONLY for UI display } } diff --git a/lib/features/transcription/domain/transcription_history_model.dart b/lib/features/transcription/domain/transcription_history_model.dart index 1f26940..4a42a57 100644 --- a/lib/features/transcription/domain/transcription_history_model.dart +++ b/lib/features/transcription/domain/transcription_history_model.dart @@ -1,33 +1,29 @@ -class TranscriptionHistoryModel { - final String transcript; +import 'medical_insights.dart'; + +class TranscriptionModel { + final String rawTranscript; final String summary; - final List symptoms; - final List medicines; - final DateTime createdAt; + final String prescription; + final MedicalInsights? insights; - TranscriptionHistoryModel({ - required this.transcript, - required this.summary, - required this.symptoms, - required this.medicines, - required this.createdAt, + const TranscriptionModel({ + this.rawTranscript = '', + this.summary = '', + this.prescription = '', + this.insights, }); - Map toJson() => { - 'transcript': transcript, - 'summary': summary, - 'symptoms': symptoms, - 'medicines': medicines, - 'createdAt': createdAt.toIso8601String(), - }; - - factory TranscriptionHistoryModel.fromJson(Map json) { - return TranscriptionHistoryModel( - transcript: json['transcript'], - summary: json['summary'], - symptoms: List.from(json['symptoms']), - medicines: List.from(json['medicines']), - createdAt: DateTime.parse(json['createdAt']), + 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 e82c943..1f3f1d6 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -1,13 +1,12 @@ -import 'dart:developer' as developer; import 'dart:async'; +import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:path_provider/path_provider.dart'; import '../data/deepgram_service.dart'; -import '../data/local_storage_service.dart'; import '../data/gemini_service.dart'; +import '../data/local_storage_service.dart'; import '../domain/transcription_model.dart'; import '../domain/medical_insights.dart'; import '../domain/transcription_history_model.dart'; @@ -18,103 +17,86 @@ class TranscriptionController extends ChangeNotifier { final _audioRecorder = AudioRecorder(); final _deepgramService = DeepgramService(); final _geminiService = GeminiService(); - final _localStorageService = LocalStorageService(); TranscriptionState state = TranscriptionState.idle; TranscriptionModel data = const TranscriptionModel(); - String? errorMessage; String _recordingPath = ''; - final List waveformValues = List.filled(40, 0.0); - Timer? _waveformTimer; bool get isRecording => state == TranscriptionState.recording; - bool get isProcessing => - state == TranscriptionState.transcribing || - state == TranscriptionState.processing; + // UI Helper Getters String get transcription => data.rawTranscript; String get summary => data.insights?.summary ?? ''; - String get prescription => data.prescription ?? ''; // FIXED: Return actual prescription - - List get symptoms => List.unmodifiable(data.insights?.symptoms ?? []); - List get medicines => List.unmodifiable(data.insights?.medicines ?? []); - - bool get hasInsights => symptoms.isNotEmpty || medicines.isNotEmpty || summary.isNotEmpty; - - Future requestPermissions() async { - final status = await Permission.microphone.request(); - if (status.isGranted) return true; - - _setError(status.isPermanentlyDenied - ? 'Microphone permission permanently denied. Please enable it in settings.' - : 'Microphone permission denied'); - return false; + List get symptoms => data.insights?.symptoms ?? []; + List get medicines => data.insights?.medicines ?? []; + + Future toggleRecording() async { + isRecording ? await _stopRecording() : await _startRecording(); } - Future _processWithGemini(String transcript) async { - // 1. Handle Empty Input Early - if (transcript.trim().isEmpty || transcript == "No speech detected") { - state = TranscriptionState.idle; + Future _startRecording() async { + try { + if (!await _audioRecorder.hasPermission()) return; + final dir = await getTemporaryDirectory(); + _recordingPath = '${dir.path}/rec_${DateTime.now().millisecondsSinceEpoch}.m4a'; + + await _audioRecorder.start(const RecordConfig(), path: _recordingPath); + state = TranscriptionState.recording; + data = const TranscriptionModel(); notifyListeners(); - return; - } + } catch (e) { _setError("Record start failed: $e"); } + } + Future _stopRecording() async { try { - state = TranscriptionState.processing; + final path = await _audioRecorder.stop(); + if (path == null) return; + + state = TranscriptionState.transcribing; notifyListeners(); + + final transcript = await _deepgramService.transcribe(path); + await _processWithGemini(transcript); + } catch (e) { _setError("Transcription failed: $e"); } + } - // Parallelize AI calls for better performance - final results = await Future.wait([ - _geminiService.generateSummary(transcript), - _geminiService.generatePrescription(transcript), - _geminiService.generateInsights(transcript), - ]); - - final String summaryText = results[0] as String; - final String prescriptionText = results[1] as String; - final MedicalInsights insights = results[2] as MedicalInsights; + Future _processWithGemini(String transcript) async { + state = TranscriptionState.processing; + notifyListeners(); + try { + // GeminiService only has generateInsights; we derive summary from it. + final insights = await _geminiService.generateInsights(transcript); + data = data.copyWith( + rawTranscript: transcript, insights: insights, - summary: summaryText, - prescription: prescriptionText, + summary: insights.summary, ); - // 2. Mark UI Done BEFORE persistence to ensure responsiveness - state = TranscriptionState.done; - notifyListeners(); - - // 3. Isolated Persistence (Doesn't break UI on failure) - _persistHistory(transcript, insights); - - } catch (e) { - _setError('Gemini error: $e'); - } - } - - Future _persistHistory(String transcript, MedicalInsights insights) async { - try { - final historyItem = TranscriptionHistoryModel( + final history = TranscriptionHistoryModel( transcript: transcript, summary: insights.summary, symptoms: insights.symptoms, medicines: insights.medicines, createdAt: DateTime.now(), ); - await _localStorageService.save(historyItem); - developer.log('History persisted successfully'); + + await _localStorageService.save(history); + state = TranscriptionState.done; } catch (e) { - developer.log('Persistence failed: $e', level: 1000); + _setError("AI Processing failed: $e"); + } finally { + notifyListeners(); } } - void _setError(String message) { - errorMessage = message; + void _setError(String msg) { + errorMessage = msg; state = TranscriptionState.error; notifyListeners(); - developer.log(message, name: 'TranscriptionController', error: message); } void checkConfigStatus(bool isLoaded) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 7415768..dd2b952 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index cf0c42e..4cb83d1 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux record_linux url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a235ca0..14e18af 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_macos import path_provider_foundation import record_darwin import share_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 359c186..e2c275e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -134,6 +134,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.23" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -160,6 +208,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 52c287a..cee651a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: share_plus: ^7.0.0 flutter_dotenv: ^5.1.0 provider: ^6.1.5+1 + flutter_secure_storage: ^9.2.2 dev_dependencies: flutter_test: From 12cce525527a7aa37ed6be6df22c45882047c57d Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 21:12:32 +0530 Subject: [PATCH 16/17] feat: add persistent transcription history with structured insights --- .../transcription_controller.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_controller.dart b/lib/features/transcription/presentation/transcription_controller.dart index 1f3f1d6..84d121e 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -24,6 +24,10 @@ class TranscriptionController extends ChangeNotifier { String? errorMessage; String _recordingPath = ''; + // FIXED: Declared missing waveform fields + final List waveformValues = List.filled(40, 0.0); + Timer? _waveformTimer; + bool get isRecording => state == TranscriptionState.recording; // UI Helper Getters @@ -63,11 +67,16 @@ class TranscriptionController extends ChangeNotifier { } Future _processWithGemini(String transcript) async { + if (transcript.isEmpty || transcript == "No speech detected") { + state = TranscriptionState.idle; + notifyListeners(); + return; + } + state = TranscriptionState.processing; notifyListeners(); try { - // GeminiService only has generateInsights; we derive summary from it. final insights = await _geminiService.generateInsights(transcript); data = data.copyWith( @@ -100,9 +109,9 @@ class TranscriptionController extends ChangeNotifier { } void checkConfigStatus(bool isLoaded) { - if (!isLoaded) { - _setError('Configuration Error: API keys could not be loaded. Please check your .env file.'); - } + if (!isLoaded) { + _setError('Configuration Error: API keys could not be loaded. Please check your .env file.'); + } } @override @@ -112,5 +121,3 @@ class TranscriptionController extends ChangeNotifier { super.dispose(); } } - - From f1ec913cbebffe2bb3ad62f9ec34e5f77f53698b Mon Sep 17 00:00:00 2001 From: shwet-07 Date: Sun, 29 Mar 2026 21:23:14 +0530 Subject: [PATCH 17/17] feat: add persistent transcription history with structured insights --- .../presentation/transcription_controller.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/features/transcription/presentation/transcription_controller.dart b/lib/features/transcription/presentation/transcription_controller.dart index 84d121e..39ceefe 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -42,7 +42,11 @@ class TranscriptionController extends ChangeNotifier { Future _startRecording() async { try { - if (!await _audioRecorder.hasPermission()) return; + if (!await _audioRecorder.hasPermission()) { + // FIXED: Provide user feedback instead of failing silently + _setError("Microphone permission is required to record audio."); + return; + } final dir = await getTemporaryDirectory(); _recordingPath = '${dir.path}/rec_${DateTime.now().millisecondsSinceEpoch}.m4a'; @@ -56,7 +60,11 @@ class TranscriptionController extends ChangeNotifier { Future _stopRecording() async { try { final path = await _audioRecorder.stop(); - if (path == null) return; + if (path == null) { + state = TranscriptionState.idle; + notifyListeners(); + return; + } state = TranscriptionState.transcribing; notifyListeners();