diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index b6fb9c72..5b529716 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -3,9 +3,9 @@ class URLs { static const String appReleaseBuildNumber = "55"; - static const host1 = "https://atomsm.hmg.com"; // production url + // static const host1 = "https://atomsm.hmg.com"; // production url // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + static const host1 = "https://atomsmuat.hmg.com"; // local UAT url static final String _baseUrl = "$_host/mobile"; // host local UAT // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis @@ -625,6 +625,7 @@ class URLs { // Copilot APIs static get copilotQueryStream => "$_baseUrl/Automation/QueryStream"; + static get copilotAudioTranscription => "$_baseUrl/Automation/AudioTranscription"; static get copilotGetQueryHistory => "$_baseUrl/Automation/GetQueryHistory"; static get predictDevice => "$_baseUrl/Automation/PredictDevice"; static copilotGetConversationByThreadId(String threadId) => "$_baseUrl/Automation/GetConversationByThreadId/$threadId"; diff --git a/lib/modules/copilot/controller/copilot_provider.dart b/lib/modules/copilot/controller/copilot_provider.dart index 46b94296..af582861 100644 --- a/lib/modules/copilot/controller/copilot_provider.dart +++ b/lib/modules/copilot/controller/copilot_provider.dart @@ -1,9 +1,12 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:developer' as log1; import 'dart:io'; import 'dart:math'; +// import 'dart:math'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/urls.dart'; @@ -15,6 +18,7 @@ class CopilotProvider extends ChangeNotifier { List messages = []; bool conversationLoading = false; bool isStreaming = false; + bool isTranscribing = false; String? sessionId; String? errorMessage; StreamSubscription? _streamSub; @@ -158,6 +162,76 @@ class CopilotProvider extends ChangeNotifier { } } + Future transcribeAudio(File audioFile, String language) async { + isTranscribing = true; + errorMessage = null; + notifyListeners(); + log1.log('i am here..'); + final exists = await audioFile.exists(); + final sizeBytes = exists ? await audioFile.length() : 0; + log1.log( + '[CopilotProvider.transcribeAudio] url=${URLs.copilotAudioTranscription} ' + 'langId=$language path=${audioFile.path} exists=$exists sizeBytes=$sizeBytes', + ); + if (!exists || sizeBytes == 0) { + errorMessage = "Recording file is empty or missing"; + isTranscribing = false; + notifyListeners(); + return null; + } + + try { + final response = await ApiManager.instance.multiPart( + URLs.copilotAudioTranscription, + body: { + "langId": language, + }, + showToast: false, + files: [ + MultipartFile.fromPath("AudioFile", audioFile.path, filename: audioFile.path.split('/').last), + ], + ); + + log1.log( + '[CopilotProvider.transcribeAudio] statusCode=${response.statusCode} ' + 'bodyLength=${response.body.length} body=${response.body}', + ); + + if (response.statusCode < 200 || response.statusCode >= 300) { + errorMessage = "Failed to transcribe audio"; + return null; + } + + return _extractTranscriptionText(response.body); + } catch (e, st) { + log1.log('[CopilotProvider.transcribeAudio] error=$e\n$st'); + errorMessage = "Failed to transcribe audio"; + return null; + } finally { + isTranscribing = false; + notifyListeners(); + } + } + + String? _extractTranscriptionText(String rawBody) { + final trimmedRaw = rawBody.trim(); + if (trimmedRaw.isEmpty) return null; + + try { + final decoded = jsonDecode(trimmedRaw); + if (decoded is String) { + return decoded.trim(); + } else if (decoded is Map) { + final text = decoded['data'] ?? decoded['text'] ?? decoded['result'] ?? decoded['transcription']; + if (text != null) return text.toString().trim(); + } + } catch (_) { + // Not JSON, fall back to raw body below. + } + + return trimmedRaw; + } + String _generateSessionId() { final rand = Random(); final bytes = List.generate(16, (_) => rand.nextInt(256)); diff --git a/lib/modules/copilot/screens/copilot_chat_page.dart b/lib/modules/copilot/screens/copilot_chat_page.dart index ceb344ec..d599a4b4 100644 --- a/lib/modules/copilot/screens/copilot_chat_page.dart +++ b/lib/modules/copilot/screens/copilot_chat_page.dart @@ -98,15 +98,26 @@ class _CopilotChatPageState extends State { setState(() => _inputState = _InputState.idle); } - void _sendRecording() async { + Future _sendRecording() async { if (_recordedFilePath == null) return; final file = File(_recordedFilePath!); - context.read().sendAudio(file, context.settingProvider.language!); + final language = context.settingProvider.language!; + final provider = context.read(); + await _recordedPlayerController.stopPlayer(); _recorderController.reset(); _recordedFilePath = null; setState(() => _inputState = _InputState.idle); - _scrollToBottom(); + + final transcribedText = await provider.transcribeAudio(file, language); + if (!mounted) return; + + if (transcribedText != null && transcribedText.isNotEmpty) { + _textController.text = transcribedText; + _textController.selection = TextSelection.fromPosition(TextPosition(offset: _textController.text.length)); + } else { + (provider.errorMessage ?? "Failed to transcribe audio. Please try again.").showToast; + } } @override @@ -237,11 +248,19 @@ class _CopilotChatPageState extends State { hintStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.lightTextColor(context)), ), ).expanded, - IconButton( - onPressed: _startRecording, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - icon: "chat_mic".toSvgAsset(width: 24, height: 24, color: AppColor.icon2Color(context)), - constraints: const BoxConstraints(), + Consumer( + builder: (_, provider, __) => IconButton( + onPressed: provider.isTranscribing ? null : _startRecording, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + icon: provider.isTranscribing + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2), + ) + : "chat_mic".toSvgAsset(width: 24, height: 24, color: AppColor.icon2Color(context)), + constraints: const BoxConstraints(), + ), ), ] else if (_inputState == _InputState.recording) ...[ AudioWaveforms( @@ -313,7 +332,7 @@ class _CopilotChatPageState extends State { }, style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), icon: Consumer( - builder: (_, provider, __) => provider.isStreaming + builder: (_, provider, __) => (provider.isStreaming || provider.isTranscribing) ? const SizedBox( height: 24, width: 24,