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/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/deepgram_service.dart b/lib/features/transcription/data/deepgram_service.dart index 4969ca8..cdacad2 100644 --- a/lib/features/transcription/data/deepgram_service.dart +++ b/lib/features/transcription/data/deepgram_service.dart @@ -11,73 +11,84 @@ class DeepgramService { String _resolveApiKey() { final configuredKey = _apiKey; - if (configuredKey != null && configuredKey.isNotEmpty) { - return configuredKey; - } + if (configuredKey != null && configuredKey.isNotEmpty) return configuredKey; + return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim(); + } - try { - return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim(); - } catch (_) { - return ''; + Future _retryPost({ + required Uri uri, + required Map headers, + required List body, + int retries = 3, + }) async { + http.Response? lastResponse; + + 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; + + lastResponse = response; + + if (response.statusCode >= 500) { + await Future.delayed(Duration(seconds: 2 * (attempt + 1))); + continue; + } else { + throw Exception('Deepgram error (${response.statusCode}): ${response.body}'); + } + } on TimeoutException { + 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( + 'Deepgram failed after $retries attempts. ' + 'Last status: ${lastResponse?.statusCode ?? "Unknown"}' + ); } 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'); + if (apiKey.isEmpty) throw Exception('Missing DEEPGRAM_API_KEY'); final file = File(recordingPath); - if (!await file.exists()) { - throw Exception('Recording file not found'); - } + if (!await file.exists()) throw Exception('Recording file not found'); final bytes = await file.readAsBytes(); + final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&smart_format=true'); - http.Response response; - try { - response = await http.post( - uri, - headers: { - 'Authorization': 'Token $apiKey', - 'Content-Type': 'audio/m4a', - }, - body: bytes, - ).timeout(const Duration(seconds: 30)); - } on TimeoutException { - throw Exception('Deepgram request timed out after 30 seconds'); - } - - if (response.statusCode == 200) { - final decodedResponse = json.decode(response.body); + final response = await _retryPost( + uri: uri, + headers: { + 'Authorization': 'Token $apiKey', + 'Content-Type': 'application/octet-stream', + }, + body: bytes, + ); - if (decodedResponse is! Map) { - throw Exception('Deepgram returned unexpected response format'); - } - - final results = decodedResponse['results']; - if (results is! Map) { - return 'No speech detected'; - } - - final channels = results['channels']; - if (channels is! List || channels.isEmpty || channels.first is! Map) { - return 'No speech detected'; - } + return _parseTranscript(response.body); + } - final alternatives = (channels.first as Map)['alternatives']; - if (alternatives is! List || alternatives.isEmpty || alternatives.first is! Map) { - return 'No speech detected'; + String _parseTranscript(String responseBody) { + try { + final decoded = json.decode(responseBody); + final transcript = decoded['results']?['channels']?[0]?['alternatives']?[0]?['transcript']; + + if (transcript is String && transcript.trim().isNotEmpty) { + return transcript.trim(); } - - final transcript = (alternatives.first as Map)['transcript']; - final result = transcript is String ? transcript.trim() : ''; - return result.isNotEmpty ? result : 'No speech detected'; - } else { - throw Exception('Deepgram failed: ${response.statusCode}'); + return 'No speech detected'; + } catch (e) { + throw Exception('Failed to parse Deepgram response: $e'); } } -} \ No newline at end of file +} diff --git a/lib/features/transcription/data/gemini_service.dart b/lib/features/transcription/data/gemini_service.dart index 34ab854..235cac0 100644 --- a/lib/features/transcription/data/gemini_service.dart +++ b/lib/features/transcription/data/gemini_service.dart @@ -1,18 +1,45 @@ +import 'dart:convert'; +import '../domain/medical_insights.dart'; import 'package:doc_pilot_new_app_gradel_fix/services/chatbot_service.dart'; class GeminiService { final ChatbotService _chatbotService = ChatbotService(); - Future generateSummary(String transcription) async { - return await _chatbotService.getGeminiResponse( - "Generate a summary of the conversation based on this transcription: $transcription", - ); + Future generateInsights(String transcription) async { + final prompt = """ +Extract structured medical information from the conversation. + +Return ONLY valid JSON in this format: +{ + "summary": "short summary", + "symptoms": ["symptom1", "symptom2"], + "medicines": ["medicine1", "medicine2"] +} + +Conversation: +$transcription +"""; + + final response = await _chatbotService.getGeminiResponse(prompt); + + try { + final cleaned = _extractJson(response); + final jsonData = json.decode(cleaned); + return MedicalInsights.fromJson(jsonData); + } catch (e) { + throw Exception('Failed to parse Gemini JSON response'); + } } - Future generatePrescription(String transcription) async { - await Future.delayed(const Duration(seconds: 3)); - return await _chatbotService.getGeminiResponse( - "Generate a prescription based on the conversation in this transcription: $transcription", - ); + // Handles messy AI responses + String _extractJson(String response) { + final start = response.indexOf('{'); + final end = response.lastIndexOf('}'); + + if (start != -1 && end != -1) { + return response.substring(start, end + 1); + } + + throw Exception('Invalid JSON format from AI'); } } \ No newline at end of file diff --git a/lib/features/transcription/data/local_storage_service.dart b/lib/features/transcription/data/local_storage_service.dart new file mode 100644 index 0000000..256ee59 --- /dev/null +++ b/lib/features/transcription/data/local_storage_service.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../domain/transcription_history_model.dart'; + +class LocalStorageService { + static const String _key = "transcription_history_secure"; + final _storage = const FlutterSecureStorage(); + + // Internal helper to get RAW list without reversing (prevents corruption) + Future> _getRawList() async { + try { + 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 []; } + } + + 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())); + } + + Future> getAll() async { + final list = await _getRawList(); + return list.reversed.toList(); // Reverse ONLY for UI display + } +} diff --git a/lib/features/transcription/domain/medical_insights.dart b/lib/features/transcription/domain/medical_insights.dart new file mode 100644 index 0000000..44984b9 --- /dev/null +++ b/lib/features/transcription/domain/medical_insights.dart @@ -0,0 +1,39 @@ +class MedicalInsights { + final String summary; + final List symptoms; + final List medicines; + + MedicalInsights({ + required this.summary, + required this.symptoms, + required this.medicines, + }); + + factory MedicalInsights.fromJson(Map json) { + return MedicalInsights( + // Coerce summary to String regardless of what the AI sends + summary: json['summary']?.toString() ?? '', + + // Safely parse lists to avoid 'type is not a subtype' errors + symptoms: _parseList(json['symptoms']), + medicines: _parseList(json['medicines']), + ); + } + + /// Helper to filter nulls and force elements to strings + static List _parseList(dynamic jsonValue) { + if (jsonValue is! List) return []; + return jsonValue + .where((item) => item != null) + .map((item) => item.toString()) + .toList(); + } + + Map toJson() { + return { + 'summary': summary, + 'symptoms': symptoms, + 'medicines': medicines, + }; + } +} diff --git a/lib/features/transcription/domain/transcription_history_model.dart b/lib/features/transcription/domain/transcription_history_model.dart new file mode 100644 index 0000000..4a42a57 --- /dev/null +++ b/lib/features/transcription/domain/transcription_history_model.dart @@ -0,0 +1,29 @@ +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, + ); + } +} diff --git a/lib/features/transcription/domain/transcription_model.dart b/lib/features/transcription/domain/transcription_model.dart index 5310208..2b64cae 100644 --- a/lib/features/transcription/domain/transcription_model.dart +++ b/lib/features/transcription/domain/transcription_model.dart @@ -1,23 +1,40 @@ -class TranscriptionModel { - final String rawTranscript; +import 'medical_insights.dart'; + +class TranscriptionHistoryModel { + final String transcript; final String summary; - final String prescription; + final List symptoms; + final List medicines; + final DateTime createdAt; - const TranscriptionModel({ - this.rawTranscript = '', - this.summary = '', - this.prescription = '', + const TranscriptionHistoryModel({ + required this.transcript, + required this.summary, + required this.symptoms, + required this.medicines, + required this.createdAt, }); - TranscriptionModel copyWith({ - String? rawTranscript, - String? summary, - String? prescription, - }) { - return TranscriptionModel( - rawTranscript: rawTranscript ?? this.rawTranscript, - summary: summary ?? this.summary, - prescription: prescription ?? this.prescription, + 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 new file mode 100644 index 0000000..9ed260b --- /dev/null +++ b/lib/features/transcription/presentation/history_screen.dart @@ -0,0 +1,93 @@ +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 = []; + bool isLoading = true; + + @override + void initState() { + super.initState(); + loadHistory(); + } + + Future loadHistory() async { + 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("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 + ), + ), + ); + }, + ); + }, + ); + } + + 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 8fddbd7..39ceefe 100644 --- a/lib/features/transcription/presentation/transcription_controller.dart +++ b/lib/features/transcription/presentation/transcription_controller.dart @@ -1,13 +1,15 @@ -import 'dart:developer' as developer; -import 'dart:math'; 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/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'; enum TranscriptionState { idle, recording, transcribing, processing, done, error } @@ -15,159 +17,115 @@ 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 = ''; - // Waveform — kept here since it's driven by recording state + // FIXED: Declared missing waveform fields 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.summary; - String get prescription => data.prescription; - - 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; - } + String get summary => data.insights?.summary ?? ''; + List get symptoms => data.insights?.symptoms ?? []; + List get medicines => data.insights?.medicines ?? []; Future toggleRecording() async { - if (isRecording) { - await _stopRecording(); - } else { - await _startRecording(); - } + isRecording ? await _stopRecording() : await _startRecording(); } Future _startRecording() async { try { if (!await _audioRecorder.hasPermission()) { - final granted = await requestPermissions(); - if (!granted) { - return; - } + // FIXED: Provide user feedback instead of failing silently + _setError("Microphone permission is required to record audio."); + 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, - ); - - // Reset previous data - data = const TranscriptionModel(); + final dir = await getTemporaryDirectory(); + _recordingPath = '${dir.path}/rec_${DateTime.now().millisecondsSinceEpoch}.m4a'; + + await _audioRecorder.start(const RecordConfig(), path: _recordingPath); state = TranscriptionState.recording; - _startWaveformAnimation(); + data = const TranscriptionModel(); notifyListeners(); - - developer.log('Started recording to: $_recordingPath'); - } catch (e) { - _setError('Error starting recording: $e'); - } + } catch (e) { _setError("Record start failed: $e"); } } Future _stopRecording() async { try { - _waveformTimer?.cancel(); - _resetWaveform(); - - await _audioRecorder.stop(); + final path = await _audioRecorder.stop(); + if (path == null) { + state = TranscriptionState.idle; + notifyListeners(); + return; + } + state = TranscriptionState.transcribing; notifyListeners(); - - developer.log('Recording stopped, transcribing...'); - await _transcribe(); - } catch (e) { - _setError('Error stopping recording: $e'); - } + + final transcript = await _deepgramService.transcribe(path); + await _processWithGemini(transcript); + } catch (e) { _setError("Transcription failed: $e"); } } - Future _transcribe() async { - try { - final transcript = await _deepgramService.transcribe(_recordingPath); - - data = data.copyWith(rawTranscript: transcript); - state = TranscriptionState.processing; + Future _processWithGemini(String transcript) async { + if (transcript.isEmpty || transcript == "No speech detected") { + state = TranscriptionState.idle; notifyListeners(); - - if (transcript.isNotEmpty && transcript != 'No speech detected') { - await _processWithGemini(transcript); - } else { - state = TranscriptionState.done; - notifyListeners(); - } - } catch (e) { - _setError('Transcription error: $e'); + return; } - } - Future _processWithGemini(String transcript) async { + state = TranscriptionState.processing; + notifyListeners(); + try { - final summary = await _geminiService.generateSummary(transcript); - final prescription = await _geminiService.generatePrescription(transcript); + final insights = await _geminiService.generateInsights(transcript); + + data = data.copyWith( + rawTranscript: transcript, + insights: insights, + summary: insights.summary, + ); - data = data.copyWith(summary: summary, prescription: prescription); - state = TranscriptionState.done; - notifyListeners(); + final history = TranscriptionHistoryModel( + transcript: transcript, + summary: insights.summary, + symptoms: insights.symptoms, + medicines: insights.medicines, + createdAt: DateTime.now(), + ); - developer.log('Gemini processing complete'); + await _localStorageService.save(history); + state = TranscriptionState.done; } 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(); - } + _setError("AI Processing failed: $e"); + } finally { notifyListeners(); - }); - } - - void _resetWaveform() { - for (int i = 0; i < waveformValues.length; i++) { - waveformValues[i] = 0.0; } } - void _setError(String message) { - errorMessage = message; + void _setError(String msg) { + errorMessage = msg; state = TranscriptionState.error; 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() { _waveformTimer?.cancel(); _audioRecorder.dispose(); super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/features/transcription/presentation/transcription_screen.dart b/lib/features/transcription/presentation/transcription_screen.dart index a957182..0c33565 100644 --- a/lib/features/transcription/presentation/transcription_screen.dart +++ b/lib/features/transcription/presentation/transcription_screen.dart @@ -1,6 +1,6 @@ -import 'package:doc_pilot_new_app_gradel_fix/screens/prescription_screen.dart'; import 'package:doc_pilot_new_app_gradel_fix/screens/summary_screen.dart'; import 'package:doc_pilot_new_app_gradel_fix/screens/transcription_detail_screen.dart'; +import 'package:doc_pilot_new_app_gradel_fix/screens/medical_insights_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'transcription_controller.dart'; @@ -32,7 +32,11 @@ class TranscriptionScreen extends StatelessWidget { children: [ const Text( 'DocPilot', - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white), + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), const SizedBox(height: 8), Text( @@ -57,7 +61,12 @@ class TranscriptionScreen extends StatelessWidget { height: value * 80 + 5, decoration: BoxDecoration( color: controller.isRecording - ? HSLColor.fromAHSL(1.0, (280 + index * 2) % 360, 0.8, 0.7 + value * 0.2).toColor() + ? HSLColor.fromAHSL( + 1.0, + (280 + index * 2) % 360, + 0.8, + 0.7 + value * 0.2, + ).toColor() : Colors.white.withOpacity(0.5), borderRadius: BorderRadius.circular(5), ), @@ -66,38 +75,48 @@ class TranscriptionScreen extends StatelessWidget { ), ), ), + const SizedBox(height: 40), - // Mic button + // Mic Button Center( child: GestureDetector( onTap: controller.isProcessing ? null : controller.toggleRecording, - child: Container( - width: 100, - height: 100, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: controller.isRecording ? Colors.red : Colors.white, - boxShadow: [ - BoxShadow( - color: (controller.isRecording ? Colors.red : Colors.white).withOpacity(0.3), - spreadRadius: 8, - blurRadius: 20, - ), - ], - ), - child: Icon( - controller.isRecording ? Icons.stop : Icons.mic, - size: 50, - color: controller.isRecording ? Colors.white : Colors.deepPurple.shade800, + child: AnimatedScale( + scale: controller.isRecording ? 1.2 : 1.0, + duration: const Duration(milliseconds: 200), + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: controller.isRecording ? Colors.red : Colors.white, + boxShadow: [ + BoxShadow( + color: (controller.isRecording ? Colors.red : Colors.white) + .withOpacity(0.3), + spreadRadius: 8, + blurRadius: 20, + ), + ], + ), + child: Icon( + controller.isRecording ? Icons.stop : Icons.mic, + size: 50, + color: controller.isRecording + ? Colors.white + : Colors.deepPurple.shade800, + ), ), ), ), ), + const SizedBox(height: 20), - // Status indicator - Center( + // Status Row + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -105,52 +124,97 @@ class TranscriptionScreen extends StatelessWidget { Container( width: 16, height: 16, - margin: const EdgeInsets.only(right: 8.0), + margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( shape: BoxShape.circle, color: controller.isRecording ? Colors.red : controller.state == TranscriptionState.processing - ? Colors.blue - : Colors.amber, + ? Colors.blue + : Colors.amber, + ), + ), + Expanded( + child: Text( + _statusDetailText(controller), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, ), ), - Text( - _statusDetailText(controller), - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white), ), ], ), ), - const SizedBox(height: 40), - // Navigation buttons + const SizedBox(height: 20), + + // Empty State (FIXED: No period in sentinel string) + if (controller.state == TranscriptionState.done && + (controller.transcription.isEmpty || + controller.transcription == "No speech detected")) + const Center( + child: Text( + "No speech detected. Try again.", + style: TextStyle(color: Colors.white70), + ), + ), + + const SizedBox(height: 20), + + // Navigation Buttons Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: ListView( children: [ _buildNavigationButton( - context, 'Transcription', Icons.record_voice_over, - controller.transcription.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => TranscriptionDetailScreen(transcription: controller.transcription), - )), + context, + 'Summary', + Icons.description, + controller.summary.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => SummaryScreen(summary: controller.summary)) + ), ), - const SizedBox(height: 16), + + const SizedBox(height: 12), + _buildNavigationButton( - context, 'Summary', Icons.summarize, - controller.summary.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => SummaryScreen(summary: controller.summary), - )), + context, + 'Full Transcription', + Icons.text_snippet, + controller.transcription.isNotEmpty && + controller.transcription != "No speech detected" && + !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => TranscriptionDetailScreen( + transcription: controller.transcription, + ), + ), + ), ), - const SizedBox(height: 16), + + const SizedBox(height: 12), + _buildNavigationButton( - context, 'Prescription', Icons.medication, - controller.prescription.isNotEmpty, - () => Navigator.push(context, MaterialPageRoute( - builder: (_) => PrescriptionScreen(prescription: controller.prescription), - )), + context, + 'Medical Insights', + Icons.analytics, + controller.summary.isNotEmpty && !controller.isProcessing, + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MedicalInsightsScreen( + symptoms: controller.symptoms, + medicines: controller.medicines, + ), + ), + ), ), ], ), @@ -163,56 +227,74 @@ class TranscriptionScreen extends StatelessWidget { ); } - String _statusText(TranscriptionState state) { - switch (state) { - case TranscriptionState.recording: return 'Recording your voice...'; - case TranscriptionState.transcribing: return 'Transcribing your voice...'; - case TranscriptionState.processing: return 'Processing with Gemini...'; - case TranscriptionState.error: return 'Something went wrong'; - default: return 'Tap the mic to begin'; - } - } - - String _statusDetailText(TranscriptionController controller) { - switch (controller.state) { - case TranscriptionState.recording: return 'Recording in progress'; - case TranscriptionState.transcribing: return 'Processing audio...'; - case TranscriptionState.processing: return 'Generating content with Gemini...'; - case TranscriptionState.done: return 'Ready to view results'; - case TranscriptionState.error: return controller.errorMessage ?? 'Error occurred'; - default: return 'Press the microphone button to start'; - } - } - Widget _buildNavigationButton( BuildContext context, - String title, + String label, IconData icon, - bool isEnabled, - VoidCallback onPressed, + bool enabled, + VoidCallback onTap, ) { - return SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: isEnabled ? onPressed : null, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: Colors.white, - foregroundColor: Colors.deepPurple, - disabledBackgroundColor: Colors.white.withOpacity(0.3), - disabledForegroundColor: Colors.white.withOpacity(0.5), - elevation: isEnabled ? 4 : 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: 24), - const SizedBox(width: 12), - Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - ], + return Material( + color: Colors.transparent, + child: InkWell( + onTap: enabled ? onTap : null, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: enabled ? Colors.white.withOpacity(0.15) : Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: enabled ? Colors.white24 : Colors.white10, + ), + ), + child: Row( + children: [ + Icon(icon, color: enabled ? Colors.white : Colors.white38), + const SizedBox(width: 16), + Text( + label, + style: TextStyle( + color: enabled ? Colors.white : Colors.white38, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + Icon( + Icons.arrow_forward_ios, + size: 16, + color: enabled ? Colors.white70 : Colors.white12, + ), + ], + ), ), ), ); } -} \ No newline at end of file + + String _statusText(TranscriptionState state) { + switch (state) { + case TranscriptionState.recording: + return 'Recording your voice...'; + case TranscriptionState.transcribing: + return 'Transcribing your voice...'; + case TranscriptionState.processing: + return 'Processing with Gemini...'; + case TranscriptionState.done: + return 'Transcription complete!'; + case TranscriptionState.error: + return 'Something went wrong'; + default: + return 'Tap the mic to begin'; + } + } + + String _statusDetailText(TranscriptionController controller) { + if (controller.isRecording) return 'Recording in progress...'; + if (controller.state == TranscriptionState.transcribing) return 'Converting speech to text...'; + if (controller.state == TranscriptionState.processing) return 'Extracting medical insights...'; + if (controller.state == TranscriptionState.done) return 'Review your insights below'; + return 'Tap the microphone to begin'; + } +} diff --git a/lib/main.dart b/lib/main.dart index c913ee0..bf7a6d4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,22 +1,37 @@ import 'dart:async'; -import 'package:doc_pilot_new_app_gradel_fix/features/transcription/presentation/transcription_controller.dart'; -import 'package:doc_pilot_new_app_gradel_fix/features/transcription/presentation/transcription_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; +import 'features/transcription/presentation/transcription_controller.dart'; +import 'features/transcription/presentation/transcription_screen.dart'; + Future main() async { - await dotenv.load(); - runApp(const MyApp()); + WidgetsFlutterBinding.ensureInitialized(); + + bool isConfigLoaded = false; + try { + // Attempt to load environment variables + await dotenv.load(fileName: ".env").timeout(const Duration(seconds: 2)); + isConfigLoaded = true; + } catch (e) { + debugPrint("Critical: Could not load .env file: $e"); + // We proceed to runApp so we can show a user-friendly error in the UI + } + + runApp(MyApp(isConfigLoaded: isConfigLoaded)); } class MyApp extends StatelessWidget { - const MyApp({super.key}); + final bool isConfigLoaded; + + const MyApp({super.key, required this.isConfigLoaded}); @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (_) => TranscriptionController(), + // Pass the config status to the controller so it can show an alert + create: (_) => TranscriptionController()..checkConfigStatus(isConfigLoaded), child: MaterialApp( title: 'DocPilot', theme: ThemeData( diff --git a/lib/screens/medical_insights_screen.dart b/lib/screens/medical_insights_screen.dart new file mode 100644 index 0000000..80aa32f --- /dev/null +++ b/lib/screens/medical_insights_screen.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class MedicalInsightsScreen extends StatelessWidget { + final List symptoms; + final List medicines; + + const MedicalInsightsScreen({ + super.key, + required this.symptoms, + required this.medicines, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Medical Insights")), + body: Padding( + padding: const EdgeInsets.all(16), + child: ListView( + children: [ + const Text( + "Symptoms", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + if (symptoms.isEmpty) + const Text("No symptoms detected") + else + ...symptoms.map( + (e) => ListTile( + leading: const Icon(Icons.warning, color: Colors.orange), + title: Text(e), + ), + ), + + const SizedBox(height: 20), + + const Text( + "Medicines", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + if (medicines.isEmpty) + const Text("No medicines suggested") + else + ...medicines.map( + (e) => ListTile( + leading: const Icon(Icons.medication, color: Colors.blue), + title: Text(e), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/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 2ddb4f7..e2c275e 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: @@ -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,30 +208,38 @@ 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: 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 +260,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 +288,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 +416,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 +553,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 +609,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 +646,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..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: @@ -65,7 +66,6 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: - # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see