Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/screenshots/mobile-overflow-issue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
177 changes: 134 additions & 43 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:record/record.dart';
import 'package:http/http.dart' as http;
import 'package:permission_handler/permission_handler.dart';
import 'dart:developer' as developer;
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

import 'services/chatbot_service.dart';
Expand All @@ -17,7 +16,11 @@ import 'screens/summary_screen.dart';
import 'screens/prescription_screen.dart';

Future<void> main() async {
await dotenv.load();
try {
await dotenv.load(fileName: '.env');
} catch (_) {
await dotenv.load(fileName: '.env.example');
}
runApp(const MyApp());
}

Expand Down Expand Up @@ -67,6 +70,25 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi
final List<double> _waveformValues = List.filled(40, 0.0);
Timer? _waveformTimer;

bool _isValidApiKey(String value, String provider) {
final trimmed = value.trim();
if (trimmed.isEmpty) {
return false;
}

final normalized = trimmed.toLowerCase();
if (provider == 'deepgram') {
return !normalized.contains('your_deepgram_api_key_here') &&
!normalized.contains('replace_with') &&
!normalized.contains('example') &&
!normalized.contains('dummy');
}

return !normalized.contains('replace_with') &&
!normalized.contains('example') &&
!normalized.contains('dummy');
}

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -187,6 +209,15 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi
Future<void> _transcribeAudio() async {
try {
final apiKey = dotenv.env['DEEPGRAM_API_KEY'] ?? '';
if (!_isValidApiKey(apiKey, 'deepgram')) {
setState(() {
_isTranscribing = false;
_isProcessing = false;
_transcription = 'DEEPGRAM_API_KEY is missing or still a placeholder. Add a real key to .env';
});
return;
}

final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2');

final file = File(_recordingPath);
Expand Down Expand Up @@ -224,6 +255,15 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi
print(_transcription);
print('=============================================');

if (!_chatbotService.hasValidApiKey) {
setState(() {
_isProcessing = false;
_summaryContent = 'Error: GEMINI_API_KEY is missing. Add it to your .env file.';
_prescriptionContent = 'Error: GEMINI_API_KEY is missing. Add it to your .env file.';
});
return;
}

// Send to Gemini for processing if we have a valid transcription
if (_transcription.isNotEmpty && _transcription != 'No speech detected') {
await _processWithGemini(_transcription);
Expand All @@ -233,9 +273,18 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi
});
}
} else {
String message = 'Transcription failed (status ${response.statusCode})';
try {
final decodedError = json.decode(response.body);
final errorText = decodedError['error'] ?? decodedError['message'];
if (errorText is String && errorText.trim().isNotEmpty) {
message = 'Transcription failed: $errorText';
}
} catch (_) {}

setState(() {
_isTranscribing = false;
_transcription = 'Transcription failed';
_transcription = message;
_isProcessing = false;
});
}
Expand Down Expand Up @@ -296,6 +345,11 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi

@override
Widget build(BuildContext context) {
final deepgramKey = dotenv.env['DEEPGRAM_API_KEY'] ?? '';
final hasDeepgramKey = _isValidApiKey(deepgramKey, 'deepgram');
final hasGeminiKey = _chatbotService.hasValidApiKey;
final setupComplete = hasDeepgramKey && hasGeminiKey;

return Scaffold(
body: Container(
decoration: BoxDecoration(
Expand All @@ -309,9 +363,10 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// App header
Expand All @@ -331,12 +386,36 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi
? 'Transcribing your voice...'
: _isProcessing
? 'Processing with Gemini...'
: 'Tap the mic to begin',
: setupComplete
? 'Tap the mic to begin'
: 'Complete API setup to enable recording and AI output',
style: const TextStyle(
fontSize: 16,
color: Colors.white70,
),
),
if (!setupComplete) ...[
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.14),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Flutter Color withOpacity deprecated 2025 2026

💡 Result:

Color.withOpacity(double) was deprecated starting in Flutter 3.27 as part of the wide-gamut color work. The old API converts a 0.0–1.0 opacity into an 8-bit alpha (0–255), which quantizes the value and can cause precision loss. Flutter now stores alpha as a floating-point value, so you should use withValues(alpha: ...) instead. [1]

Replace this

final c = Colors.blue.withOpacity(0.5);

With this

final c = Colors.blue.withValues(alpha: 0.5);

[1]

If you intentionally want the old 8-bit/quantized behavior

Use withAlpha (0–255):

final c = Colors.blue.withAlpha((0.5 * 255).round());

[1][2]

Sources: Flutter wide-gamut migration guide (opacity section) [1], Flutter API docs for Color.withOpacity (shows it rounds to 8-bit alpha) [2], Flutter GitHub deprecation message (“Use .withValues() to avoid precision loss.”) [3]

[1] (docs.flutter.dev)
[2] (api.flutter.dev)
[3] (github.com)

Citations:


🏁 Script executed:

cat pubspec.yaml | head -30

Repository: AOSSIE-Org/DocPilot

Length of output: 1817


🏁 Script executed:

rg "withOpacity" lib/main.dart -n

Repository: AOSSIE-Org/DocPilot

Length of output: 440


Replace deprecated Color.withOpacity with Color.withValues(alpha: ...)

Color.withOpacity was deprecated in Flutter 3.27 due to precision loss from 8-bit alpha quantization. The new withValues(alpha: ...) API uses floating-point alpha values. Replace all instances:

  • Line 403: Colors.white.withOpacity(0.14)Colors.white.withValues(alpha: 0.14)
  • Line 447: Colors.white.withOpacity(0.5)Colors.white.withValues(alpha: 0.5)
  • Line 484: (...).withOpacity(0.3)(...).withValues(alpha: 0.3)
  • Line 612: Colors.white.withOpacity(0.3)Colors.white.withValues(alpha: 0.3)
  • Line 613: Colors.white.withOpacity(0.5)Colors.white.withValues(alpha: 0.5)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/main.dart` at line 403, Replace deprecated Color.withOpacity usages with
the new withValues(alpha: ...) API: change the occurrences of
Colors.white.withOpacity(0.14) to Colors.white.withValues(alpha: 0.14),
Colors.white.withOpacity(0.5) to Colors.white.withValues(alpha: 0.5), any
instance like (<expression>).withOpacity(0.3) to
(<expression>).withValues(alpha: 0.3), and the other two
Colors.white.withOpacity(0.3) and Colors.white.withOpacity(0.5) similarly;
locate these by searching for "withOpacity(" in the file (e.g., the color:
Colors.white.withOpacity(0.14) property and the other three withOpacity calls)
and replace each call signature to use withValues(alpha: <same float>)
preserving the original alpha values.

borderRadius: BorderRadius.circular(10),
),
child: Text(
'Missing valid API keys:\n'
'- DEEPGRAM_API_KEY (${hasDeepgramKey ? 'OK' : 'Missing/placeholder'})\n'
'- GEMINI_API_KEY (${hasGeminiKey ? 'OK' : 'Missing/placeholder'})\n\n'
'Create .env in the project root with real keys.',
style: const TextStyle(
fontSize: 13,
color: Colors.white,
height: 1.3,
),
),
),
],
const SizedBox(height: 30),

// Waveform visualization
Expand Down Expand Up @@ -380,7 +459,20 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi
// Microphone button
Center(
child: GestureDetector(
onTap: (_isTranscribing || _isProcessing) ? null : _toggleRecording,
onTap: (_isTranscribing || _isProcessing)
? null
: () {
if (!hasDeepgramKey) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Add a valid DEEPGRAM_API_KEY in .env first.'),
backgroundColor: Colors.red,
),
);
return;
}
_toggleRecording();
},
child: Container(
width: 100,
height: 100,
Expand Down Expand Up @@ -449,56 +541,55 @@ class _TranscriptionScreenState extends State<TranscriptionScreen> with SingleTi
const SizedBox(height: 40),

// Vertical navigation buttons
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildNavigationButton(
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildNavigationButton(
context,
'Transcription',
Icons.record_voice_over,
_formattedTranscription.isNotEmpty,
() => Navigator.push(
context,
'Transcription',
Icons.record_voice_over,
_formattedTranscription.isNotEmpty,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TranscriptionDetailScreen(transcription: _formattedTranscription),
),
MaterialPageRoute(
builder: (context) => TranscriptionDetailScreen(transcription: _formattedTranscription),
),
),
const SizedBox(height: 16),
_buildNavigationButton(
),
const SizedBox(height: 16),
_buildNavigationButton(
context,
'Summary',
Icons.summarize,
_summaryContent.isNotEmpty,
() => Navigator.push(
context,
'Summary',
Icons.summarize,
_summaryContent.isNotEmpty,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SummaryScreen(summary: _summaryContent),
),
MaterialPageRoute(
builder: (context) => SummaryScreen(summary: _summaryContent),
),
),
const SizedBox(height: 16),
_buildNavigationButton(
),
const SizedBox(height: 16),
_buildNavigationButton(
context,
'Prescription',
Icons.medication,
_prescriptionContent.isNotEmpty,
() => Navigator.push(
context,
'Prescription',
Icons.medication,
_prescriptionContent.isNotEmpty,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PrescriptionScreen(prescription: _prescriptionContent),
),
MaterialPageRoute(
builder: (context) => PrescriptionScreen(prescription: _prescriptionContent),
),
),
],
),
),
],
),
],
),
),
),
),
),
);
}

Expand Down
66 changes: 54 additions & 12 deletions lib/services/chatbot_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,28 @@ class ChatbotService {
// Get API key from .env file
final String apiKey = dotenv.env['GEMINI_API_KEY'] ?? '';

bool get hasValidApiKey => _isValidApiKey(apiKey);

bool _isValidApiKey(String value) {
final trimmed = value.trim();
if (trimmed.isEmpty) {
return false;
}

final normalized = trimmed.toLowerCase();
return !normalized.contains('your_gemini_api_key_here') &&
!normalized.contains('replace_with') &&
!normalized.contains('example') &&
!normalized.contains('dummy');
}

// Get a response from Gemini based on a prompt
Future<String> getGeminiResponse(String prompt) async {
print('\n=== GEMINI PROMPT ===');
print(prompt);
if (!hasValidApiKey) {
return 'Error: GEMINI_API_KEY is missing. Add it to your .env file.';
}

developer.log('Sending prompt to Gemini', name: 'ChatbotService');

final url = Uri.parse('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=$apiKey');

Expand All @@ -25,23 +43,47 @@ class ChatbotService {
"maxOutputTokens": 1024
}
}),
);
).timeout(const Duration(seconds: 30));

if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final result = data['candidates'][0]['content']['parts'][0]['text'];

print('\n=== GEMINI RESPONSE ===');
print(result);
final candidates = data['candidates'];
if (candidates is List && candidates.isNotEmpty) {
final content = candidates[0]['content'];
final parts = content?['parts'];
if (parts is List && parts.isNotEmpty) {
final result = parts[0]['text'];
if (result is String && result.trim().isNotEmpty) {
developer.log('Gemini response received', name: 'ChatbotService');
return result;
}
}
}

return result;
developer.log(
'Gemini response format was not as expected: ${response.body}',
name: 'ChatbotService',
);
return 'Error: Received an unexpected response format from Gemini.';
} else {
print('API Error: ${response.statusCode}');
return "Error: Could not generate response. Status code: ${response.statusCode}";
String errorMessage = 'Error: Gemini API request failed (status ${response.statusCode}).';
try {
final data = jsonDecode(response.body);
final apiMessage = data['error']?['message'];
if (apiMessage is String && apiMessage.trim().isNotEmpty) {
errorMessage = 'Error: $apiMessage';
}
} catch (_) {}

developer.log(
'Gemini API error: ${response.statusCode} ${response.body}',
name: 'ChatbotService',
);
return errorMessage;
}
} catch (e) {
print('Exception: $e');
return "Error: Could not connect to API: $e";
developer.log('Gemini request failed: $e', name: 'ChatbotService', error: e);
return 'Error: Could not connect to Gemini API. Please try again.';
}
}
}
Loading