From 61d20d20fdeba506ada8597dbfe2ab6862827f3f Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 21 Oct 2026 16:08:30 +0300 Subject: [PATCH] image attachment added cont. --- lib/controllers/api_routes/api_manager.dart | 15 +- .../copilot/controller/copilot_provider.dart | 5 +- .../copilot/screens/copilot_chat_page.dart | 322 +++++++++++------- 3 files changed, 217 insertions(+), 125 deletions(-) diff --git a/lib/controllers/api_routes/api_manager.dart b/lib/controllers/api_routes/api_manager.dart index 2aa3a88d..a343c506 100644 --- a/lib/controllers/api_routes/api_manager.dart +++ b/lib/controllers/api_routes/api_manager.dart @@ -189,18 +189,18 @@ class ApiManager { request.headers.addAll(headers); http.StreamedResponse streamedResponse = await _httpClient.send(request); - + // Handle 401 immediately if (streamedResponse.statusCode == 401) { showLoginDialog(); } - + return streamedResponse; } /// POST FormData request that returns StreamedResponse for progress tracking /// This method is specifically for form-data requests (multipart/form-data) - Future postFormDataStreaming(String url, {Map? headers, required Map body}) async { + Future postFormDataStreaming(String url, {Map? headers, required Map body, File? file}) async { // Create headers for form data Map formHeaders = { 'X-Timezone-Offset': DateTime.now().timeZoneOffset.toString().split(".").first, @@ -221,6 +221,9 @@ class ApiManager { var request = http.MultipartRequest('POST', url0); request.fields.addAll(body); request.headers.addAll(headers); + if (file != null) { + request.files.add(await MultipartFile.fromPath('ImageFile', file.path)); + } http.StreamedResponse streamedResponse = await _httpClient.send(request); @@ -468,7 +471,7 @@ class ApiManager { return response; } - Future multiPart(String url, {Map? headers, required Map body, bool showToast=true, required List> files}) async { + Future multiPart(String url, {Map? headers, required Map body, bool showToast = true, required List> files}) async { Map headers0 = _headers; headers0["Content-Type"] = 'multipart/form-data'; @@ -502,7 +505,7 @@ class ApiManager { final responseBody = jsonDecode(response.body); if (responseBody is Map) { final message = responseBody["message"]; // Reuse parsed body - if (message != null && message.toString().isNotEmpty&&showToast) { + if (message != null && message.toString().isNotEmpty && showToast) { Fluttertoast.showToast(msg: message, toastLength: Toast.LENGTH_LONG); } } @@ -560,4 +563,4 @@ class ApiManager { void dispose() { _httpClient.close(); } -} \ No newline at end of file +} diff --git a/lib/modules/copilot/controller/copilot_provider.dart b/lib/modules/copilot/controller/copilot_provider.dart index 59a87860..91e7182e 100644 --- a/lib/modules/copilot/controller/copilot_provider.dart +++ b/lib/modules/copilot/controller/copilot_provider.dart @@ -33,6 +33,7 @@ class CopilotProvider extends ChangeNotifier { HubConnection? _pttHub; PttSignalRHttpClient? _pttHttpClient; + // Serializes connect/disconnect so they never overlap when the user navigates // in and out of the Push-to-Talk screen quickly. Future _pttOpQueue = Future.value(); @@ -147,7 +148,7 @@ class CopilotProvider extends ChangeNotifier { notifyListeners(); } - Future sendText(String query, String language) async { + Future sendText(String query, String language, File? image) async { if (isStreaming || query.trim().isEmpty) return; sessionId ??= _generateSessionId(); @@ -163,11 +164,13 @@ class CopilotProvider extends ChangeNotifier { URLs.copilotQueryStream, body: { "sessionId": sessionId!, + "isImage": "${image != null}", "isAudio": "false", "langId": "$language", 'isFromMobile': 'true', "query": query, }, + file: image, ); await _consumeStream(streamedResponse, assistantMsg, null); } catch (e) { diff --git a/lib/modules/copilot/screens/copilot_chat_page.dart b/lib/modules/copilot/screens/copilot_chat_page.dart index a11583db..f1c60a6d 100644 --- a/lib/modules/copilot/screens/copilot_chat_page.dart +++ b/lib/modules/copilot/screens/copilot_chat_page.dart @@ -13,6 +13,7 @@ import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/copilot/screens/copilot_history_page.dart'; import 'package:test_sa/modules/copilot/screens/copilot_live_talk_page.dart'; import 'package:test_sa/modules/copilot/screens/copilot_push_to_talk_page.dart'; +import 'package:test_sa/modules/cx_module/chat/helper/chat_file_picker.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/thinking_orbs/src/thinking_orb.dart'; @@ -39,6 +40,8 @@ class _CopilotChatPageState extends State { _InputState _inputState = _InputState.idle; String? _recordedFilePath; + File? image; + CopilotProvider? _copilotProvider; @override @@ -73,7 +76,9 @@ class _CopilotChatPageState extends State { final text = _textController.text.trim(); if (text.isEmpty) return; _textController.clear(); - context.read().sendText(text, context.settingProvider.language!); + context.read().sendText(text, context.settingProvider.language!, image); + image = null; + setState(() {}); _scrollToBottom(); } @@ -228,7 +233,7 @@ class _CopilotChatPageState extends State { child: ConstrainedBox( constraints: const BoxConstraints(minHeight: 56), child: Container( - height: 50, + // height: 50, margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColor.background(context), @@ -240,125 +245,206 @@ class _CopilotChatPageState extends State { reserveGlowSpace: false, borderRadius: 35, strength: 0.7, - child: Row( + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - if (_inputState == _InputState.idle) ...[ - TextFormField( - controller: _textController, - cursorColor: AppColor.loadingColor(context), - style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)), - minLines: 1, - maxLines: 3, - textInputAction: TextInputAction.none, - keyboardType: TextInputType.multiline, - decoration: InputDecoration( - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - border: InputBorder.none, - errorBorder: InputBorder.none, - contentPadding: const EdgeInsets.only(left: 16, top: 8, bottom: 8), - alignLabelWithHint: true, - constraints: const BoxConstraints(), - suffixIconConstraints: const BoxConstraints(), - hintText: "Ask anything...", - hintStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.lightTextColor(context)), - ), - ).expanded, - 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(), + if (image != null) + SizedBox( + width: 80.toScreenWidth, + height: 80.toScreenWidth, + child: Stack( + // alignment: AlignmentDirectional.topEnd, + children: [ + Positioned.fill( + child: Container( + margin: EdgeInsetsDirectional.only(top: 8.toScreenHeight, end: 8.toScreenWidth), + decoration: BoxDecoration( + color: AppColor.neutral90, + border: Border.all(width: 1, color: AppColor.border2Color(context)), + image: DecorationImage( + image: FileImage(image!), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(12), + ), + child: MaterialButton( + padding: EdgeInsets.zero, + onPressed: () async { + context.fullScreenImageViewer(Image.file(image!)); + }, + ), + )), + if (image != null) + Positioned( + top: 4, + right: 4, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear), + iconSize: 18, + style: IconButton.styleFrom(elevation: 2, tapTargetSize: MaterialTapTargetSize.shrinkWrap, backgroundColor: AppColor.red30, foregroundColor: Colors.white), + visualDensity: VisualDensity.compact, + constraints: const BoxConstraints(), + onPressed: () { + setState(() { + image = null; + }); + }, + )), + ], ), ), - ] else if (_inputState == _InputState.recording) ...[ - AudioWaveforms( - size: Size(MediaQuery.of(context).size.width, 24.0), - waveStyle: const WaveStyle(waveColor: AppColor.neutral50, extendWaveform: true, showMiddleLine: false), - padding: const EdgeInsets.only(left: 16), - recorderController: _recorderController, - ).expanded, - IconButton( - onPressed: _stopRecording, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - icon: const Icon(Icons.stop_circle_rounded), - constraints: const BoxConstraints(), - ), - ] else ...[ - if (_recordedPlayerController.playerState == PlayerState.playing) - IconButton( - onPressed: () async { - await _recordedPlayerController.pausePlayer(); - await _recordedPlayerController.stopPlayer(); - setState(() {}); - }, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - icon: const Icon(Icons.stop_circle_outlined, size: 20), - constraints: const BoxConstraints(), - ) - else - IconButton( - onPressed: () async { - await _recordedPlayerController.preparePlayer(path: _recordedFilePath!); - await _recordedPlayerController.startPlayer(); - setState(() {}); - }, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - icon: const Icon(Icons.play_circle_fill_rounded, size: 20), - constraints: const BoxConstraints(), - ), - AudioFileWaveforms( - playerController: _recordedPlayerController, - waveformData: _recorderController.waveData, - enableSeekGesture: false, - continuousWaveform: false, - waveformType: WaveformType.long, - playerWaveStyle: const PlayerWaveStyle( - fixedWaveColor: AppColor.neutral50, - liveWaveColor: AppColor.primary10, - showSeekLine: true, - ), - size: Size(MediaQuery.of(context).size.width, 24.0), - ).expanded, - IconButton( - onPressed: _cancelRecording, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - icon: "delete_icon".toSvgAsset(width: 24, height: 24), - constraints: const BoxConstraints(), - ), - ], - if (_inputState != _InputState.recording) - IconButton( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - onPressed: () { - if (_inputState == _InputState.recorded) { - _sendRecording(); - } else { - _sendText(); - } - }, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - icon: Consumer( - builder: (_, provider, __) => (provider.isStreaming || provider.isTranscribing) - ? const SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2), - ) - : "copilot_send_msg".toSvgAsset(width: 24, height: 24), - ), - constraints: const BoxConstraints(), - ), - 8.width, + Row( + children: [ + if (_inputState == _InputState.idle) ...[ + TextFormField( + controller: _textController, + cursorColor: AppColor.loadingColor(context), + style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.textStyleColor(context)), + minLines: 1, + maxLines: 2, + textInputAction: TextInputAction.none, + keyboardType: TextInputType.multiline, + decoration: InputDecoration( + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + border: InputBorder.none, + errorBorder: InputBorder.none, + contentPadding: const EdgeInsets.only(left: 16, top: 8, bottom: 8), + alignLabelWithHint: true, + constraints: const BoxConstraints(), + suffixIconConstraints: const BoxConstraints(), + hintText: "Ask anything...", + hintStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.lightTextColor(context)), + ), + ).expanded, + // if (image == null) + // IconButton( + // onPressed: () async { + // FocusScope.of(context).unfocus(); + // File? file = (await showModalBottomSheet( + // context: context, + // backgroundColor: AppColor.bottomSheetColor(context), + // shape: const RoundedRectangleBorder( + // borderRadius: BorderRadius.vertical( + // top: Radius.circular(20), + // ), + // ), + // clipBehavior: Clip.antiAliasWithSaveLayer, + // builder: (BuildContext context) => ChatFilePicker())) as File?; + // if (file != null) { + // image = file; + // setState(() {}); + // } + // }, + // style: const ButtonStyle( + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded + // ), + // icon: "chat_attachment".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 + ? ThinkingOrb( + size: 20, + state: ThinkingOrbState.working, + theme: context.isDark ? ThinkingOrbTheme.dark : ThinkingOrbTheme.light, + ) + // 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( + size: Size(MediaQuery.of(context).size.width, 24.0), + waveStyle: const WaveStyle(waveColor: AppColor.neutral50, extendWaveform: true, showMiddleLine: false), + padding: const EdgeInsets.only(left: 16), + recorderController: _recorderController, + ).expanded, + IconButton( + onPressed: _stopRecording, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + icon: const Icon(Icons.stop_circle_rounded), + constraints: const BoxConstraints(), + ), + ] else ...[ + if (_recordedPlayerController.playerState == PlayerState.playing) + IconButton( + onPressed: () async { + await _recordedPlayerController.pausePlayer(); + await _recordedPlayerController.stopPlayer(); + setState(() {}); + }, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + icon: const Icon(Icons.stop_circle_outlined, size: 20), + constraints: const BoxConstraints(), + ) + else + IconButton( + onPressed: () async { + await _recordedPlayerController.preparePlayer(path: _recordedFilePath!); + await _recordedPlayerController.startPlayer(); + setState(() {}); + }, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + icon: const Icon(Icons.play_circle_fill_rounded, size: 20), + constraints: const BoxConstraints(), + ), + AudioFileWaveforms( + playerController: _recordedPlayerController, + waveformData: _recorderController.waveData, + enableSeekGesture: false, + continuousWaveform: false, + waveformType: WaveformType.long, + playerWaveStyle: const PlayerWaveStyle( + fixedWaveColor: AppColor.neutral50, + liveWaveColor: AppColor.primary10, + showSeekLine: true, + ), + size: Size(MediaQuery.of(context).size.width, 24.0), + ).expanded, + IconButton( + onPressed: _cancelRecording, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + icon: "delete_icon".toSvgAsset(width: 24, height: 24), + constraints: const BoxConstraints(), + ), + ], + if (_inputState != _InputState.recording) + IconButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + onPressed: () { + if (_inputState == _InputState.recorded) { + _sendRecording(); + } else { + _sendText(); + } + }, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + icon: Consumer( + builder: (_, provider, __) => (provider.isStreaming || provider.isTranscribing) + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2), + ) + : "copilot_send_msg".toSvgAsset(width: 24, height: 24), + ), + constraints: const BoxConstraints(), + ), + 8.width, + ], + ), ], ), ),