image attachment added cont.

main_production_copilot
Sikander Saleem 2 weeks ago
parent 994acccc9d
commit 61d20d20fd

@ -200,7 +200,7 @@ class ApiManager {
/// POST FormData request that returns StreamedResponse for progress tracking /// POST FormData request that returns StreamedResponse for progress tracking
/// This method is specifically for form-data requests (multipart/form-data) /// This method is specifically for form-data requests (multipart/form-data)
Future<http.StreamedResponse> postFormDataStreaming(String url, {Map<String, String>? headers, required Map<String, String> body}) async { Future<http.StreamedResponse> postFormDataStreaming(String url, {Map<String, String>? headers, required Map<String, String> body, File? file}) async {
// Create headers for form data // Create headers for form data
Map<String, String> formHeaders = { Map<String, String> formHeaders = {
'X-Timezone-Offset': DateTime.now().timeZoneOffset.toString().split(".").first, 'X-Timezone-Offset': DateTime.now().timeZoneOffset.toString().split(".").first,
@ -221,6 +221,9 @@ class ApiManager {
var request = http.MultipartRequest('POST', url0); var request = http.MultipartRequest('POST', url0);
request.fields.addAll(body); request.fields.addAll(body);
request.headers.addAll(headers); request.headers.addAll(headers);
if (file != null) {
request.files.add(await MultipartFile.fromPath('ImageFile', file.path));
}
http.StreamedResponse streamedResponse = await _httpClient.send(request); http.StreamedResponse streamedResponse = await _httpClient.send(request);
@ -468,7 +471,7 @@ class ApiManager {
return response; return response;
} }
Future<http.Response> multiPart(String url, {Map<String, String>? headers, required Map<String, String> body, bool showToast=true, required List<Future<MultipartFile>> files}) async { Future<http.Response> multiPart(String url, {Map<String, String>? headers, required Map<String, String> body, bool showToast = true, required List<Future<MultipartFile>> files}) async {
Map<String, String> headers0 = _headers; Map<String, String> headers0 = _headers;
headers0["Content-Type"] = 'multipart/form-data'; headers0["Content-Type"] = 'multipart/form-data';
@ -502,7 +505,7 @@ class ApiManager {
final responseBody = jsonDecode(response.body); final responseBody = jsonDecode(response.body);
if (responseBody is Map<String, dynamic>) { if (responseBody is Map<String, dynamic>) {
final message = responseBody["message"]; // Reuse parsed body 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); Fluttertoast.showToast(msg: message, toastLength: Toast.LENGTH_LONG);
} }
} }

@ -33,6 +33,7 @@ class CopilotProvider extends ChangeNotifier {
HubConnection? _pttHub; HubConnection? _pttHub;
PttSignalRHttpClient? _pttHttpClient; PttSignalRHttpClient? _pttHttpClient;
// Serializes connect/disconnect so they never overlap when the user navigates // Serializes connect/disconnect so they never overlap when the user navigates
// in and out of the Push-to-Talk screen quickly. // in and out of the Push-to-Talk screen quickly.
Future<void> _pttOpQueue = Future.value(); Future<void> _pttOpQueue = Future.value();
@ -147,7 +148,7 @@ class CopilotProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> sendText(String query, String language) async { Future<void> sendText(String query, String language, File? image) async {
if (isStreaming || query.trim().isEmpty) return; if (isStreaming || query.trim().isEmpty) return;
sessionId ??= _generateSessionId(); sessionId ??= _generateSessionId();
@ -163,11 +164,13 @@ class CopilotProvider extends ChangeNotifier {
URLs.copilotQueryStream, URLs.copilotQueryStream,
body: { body: {
"sessionId": sessionId!, "sessionId": sessionId!,
"isImage": "${image != null}",
"isAudio": "false", "isAudio": "false",
"langId": "$language", "langId": "$language",
'isFromMobile': 'true', 'isFromMobile': 'true',
"query": query, "query": query,
}, },
file: image,
); );
await _consumeStream(streamedResponse, assistantMsg, null); await _consumeStream(streamedResponse, assistantMsg, null);
} catch (e) { } catch (e) {

@ -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_history_page.dart';
import 'package:test_sa/modules/copilot/screens/copilot_live_talk_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/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/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/thinking_orbs/src/thinking_orb.dart'; import 'package:test_sa/thinking_orbs/src/thinking_orb.dart';
@ -39,6 +40,8 @@ class _CopilotChatPageState extends State<CopilotChatPage> {
_InputState _inputState = _InputState.idle; _InputState _inputState = _InputState.idle;
String? _recordedFilePath; String? _recordedFilePath;
File? image;
CopilotProvider? _copilotProvider; CopilotProvider? _copilotProvider;
@override @override
@ -73,7 +76,9 @@ class _CopilotChatPageState extends State<CopilotChatPage> {
final text = _textController.text.trim(); final text = _textController.text.trim();
if (text.isEmpty) return; if (text.isEmpty) return;
_textController.clear(); _textController.clear();
context.read<CopilotProvider>().sendText(text, context.settingProvider.language!); context.read<CopilotProvider>().sendText(text, context.settingProvider.language!, image);
image = null;
setState(() {});
_scrollToBottom(); _scrollToBottom();
} }
@ -228,7 +233,7 @@ class _CopilotChatPageState extends State<CopilotChatPage> {
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 56), constraints: const BoxConstraints(minHeight: 56),
child: Container( child: Container(
height: 50, // height: 50,
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.background(context), color: AppColor.background(context),
@ -240,125 +245,206 @@ class _CopilotChatPageState extends State<CopilotChatPage> {
reserveGlowSpace: false, reserveGlowSpace: false,
borderRadius: 35, borderRadius: 35,
strength: 0.7, strength: 0.7,
child: Row( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
if (_inputState == _InputState.idle) ...[ if (image != null)
TextFormField( SizedBox(
controller: _textController, width: 80.toScreenWidth,
cursorColor: AppColor.loadingColor(context), height: 80.toScreenWidth,
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)), child: Stack(
minLines: 1, // alignment: AlignmentDirectional.topEnd,
maxLines: 3, children: [
textInputAction: TextInputAction.none, Positioned.fill(
keyboardType: TextInputType.multiline, child: Container(
decoration: InputDecoration( margin: EdgeInsetsDirectional.only(top: 8.toScreenHeight, end: 8.toScreenWidth),
enabledBorder: InputBorder.none, decoration: BoxDecoration(
focusedBorder: InputBorder.none, color: AppColor.neutral90,
border: InputBorder.none, border: Border.all(width: 1, color: AppColor.border2Color(context)),
errorBorder: InputBorder.none, image: DecorationImage(
contentPadding: const EdgeInsets.only(left: 16, top: 8, bottom: 8), image: FileImage(image!),
alignLabelWithHint: true, fit: BoxFit.cover,
constraints: const BoxConstraints(), ),
suffixIconConstraints: const BoxConstraints(), borderRadius: BorderRadius.circular(12),
hintText: "Ask anything...", ),
hintStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.lightTextColor(context)), child: MaterialButton(
), padding: EdgeInsets.zero,
).expanded, onPressed: () async {
Consumer<CopilotProvider>( context.fullScreenImageViewer(Image.file(image!));
builder: (_, provider, __) => IconButton( },
onPressed: provider.isTranscribing ? null : _startRecording, ),
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), )),
icon: provider.isTranscribing if (image != null)
? const SizedBox( Positioned(
height: 20, top: 4,
width: 20, right: 4,
child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2), child: IconButton(
) padding: EdgeInsets.zero,
: "chat_mic".toSvgAsset(width: 24, height: 24, color: AppColor.icon2Color(context)), icon: const Icon(Icons.clear),
constraints: const BoxConstraints(), 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) ...[ Row(
AudioWaveforms( children: [
size: Size(MediaQuery.of(context).size.width, 24.0), if (_inputState == _InputState.idle) ...[
waveStyle: const WaveStyle(waveColor: AppColor.neutral50, extendWaveform: true, showMiddleLine: false), TextFormField(
padding: const EdgeInsets.only(left: 16), controller: _textController,
recorderController: _recorderController, cursorColor: AppColor.loadingColor(context),
).expanded, style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.textStyleColor(context)),
IconButton( minLines: 1,
onPressed: _stopRecording, maxLines: 2,
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), textInputAction: TextInputAction.none,
icon: const Icon(Icons.stop_circle_rounded), keyboardType: TextInputType.multiline,
constraints: const BoxConstraints(), decoration: InputDecoration(
), enabledBorder: InputBorder.none,
] else ...[ focusedBorder: InputBorder.none,
if (_recordedPlayerController.playerState == PlayerState.playing) border: InputBorder.none,
IconButton( errorBorder: InputBorder.none,
onPressed: () async { contentPadding: const EdgeInsets.only(left: 16, top: 8, bottom: 8),
await _recordedPlayerController.pausePlayer(); alignLabelWithHint: true,
await _recordedPlayerController.stopPlayer(); constraints: const BoxConstraints(),
setState(() {}); suffixIconConstraints: const BoxConstraints(),
}, hintText: "Ask anything...",
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), hintStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.lightTextColor(context)),
icon: const Icon(Icons.stop_circle_outlined, size: 20), ),
constraints: const BoxConstraints(), ).expanded,
) // if (image == null)
else // IconButton(
IconButton( // onPressed: () async {
onPressed: () async { // FocusScope.of(context).unfocus();
await _recordedPlayerController.preparePlayer(path: _recordedFilePath!); // File? file = (await showModalBottomSheet(
await _recordedPlayerController.startPlayer(); // context: context,
setState(() {}); // backgroundColor: AppColor.bottomSheetColor(context),
}, // shape: const RoundedRectangleBorder(
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), // borderRadius: BorderRadius.vertical(
icon: const Icon(Icons.play_circle_fill_rounded, size: 20), // top: Radius.circular(20),
constraints: const BoxConstraints(), // ),
), // ),
AudioFileWaveforms( // clipBehavior: Clip.antiAliasWithSaveLayer,
playerController: _recordedPlayerController, // builder: (BuildContext context) => ChatFilePicker())) as File?;
waveformData: _recorderController.waveData, // if (file != null) {
enableSeekGesture: false, // image = file;
continuousWaveform: false, // setState(() {});
waveformType: WaveformType.long, // }
playerWaveStyle: const PlayerWaveStyle( // },
fixedWaveColor: AppColor.neutral50, // style: const ButtonStyle(
liveWaveColor: AppColor.primary10, // tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
showSeekLine: true, // ),
), // icon: "chat_attachment".toSvgAsset(width: 24, height: 24, color: AppColor.icon2Color(context)),
size: Size(MediaQuery.of(context).size.width, 24.0), // constraints: const BoxConstraints(),
).expanded, // ),
IconButton( Consumer<CopilotProvider>(
onPressed: _cancelRecording, builder: (_, provider, __) => IconButton(
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), onPressed: provider.isTranscribing ? null : _startRecording,
icon: "delete_icon".toSvgAsset(width: 24, height: 24), style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
constraints: const BoxConstraints(), icon: provider.isTranscribing
), ? ThinkingOrb(
], size: 20,
if (_inputState != _InputState.recording) state: ThinkingOrbState.working,
IconButton( theme: context.isDark ? ThinkingOrbTheme.dark : ThinkingOrbTheme.light,
splashColor: Colors.transparent, )
highlightColor: Colors.transparent, // SizedBox(
hoverColor: Colors.transparent, // height: 20,
onPressed: () { // width: 20,
if (_inputState == _InputState.recorded) { // child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2),
_sendRecording(); // )
} else { : "chat_mic".toSvgAsset(width: 24, height: 24, color: AppColor.icon2Color(context)),
_sendText(); constraints: const BoxConstraints(),
} ),
}, ),
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), ] else if (_inputState == _InputState.recording) ...[
icon: Consumer<CopilotProvider>( AudioWaveforms(
builder: (_, provider, __) => (provider.isStreaming || provider.isTranscribing) size: Size(MediaQuery.of(context).size.width, 24.0),
? const SizedBox( waveStyle: const WaveStyle(waveColor: AppColor.neutral50, extendWaveform: true, showMiddleLine: false),
height: 24, padding: const EdgeInsets.only(left: 16),
width: 24, recorderController: _recorderController,
child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2), ).expanded,
) IconButton(
: "copilot_send_msg".toSvgAsset(width: 24, height: 24), onPressed: _stopRecording,
), style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
constraints: const BoxConstraints(), icon: const Icon(Icons.stop_circle_rounded),
), constraints: const BoxConstraints(),
8.width, ),
] 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<CopilotProvider>(
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,
],
),
], ],
), ),
), ),

Loading…
Cancel
Save