image attachment added cont.

main_production_copilot
Sikander Saleem 1 day ago
parent 994acccc9d
commit 61d20d20fd

@ -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<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
Map<String, String> 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<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;
headers0["Content-Type"] = 'multipart/form-data';
@ -502,7 +505,7 @@ class ApiManager {
final responseBody = jsonDecode(response.body);
if (responseBody is Map<String, dynamic>) {
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();
}
}
}

@ -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<void> _pttOpQueue = Future.value();
@ -147,7 +148,7 @@ class CopilotProvider extends ChangeNotifier {
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;
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) {

@ -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<CopilotChatPage> {
_InputState _inputState = _InputState.idle;
String? _recordedFilePath;
File? image;
CopilotProvider? _copilotProvider;
@override
@ -73,7 +76,9 @@ class _CopilotChatPageState extends State<CopilotChatPage> {
final text = _textController.text.trim();
if (text.isEmpty) return;
_textController.clear();
context.read<CopilotProvider>().sendText(text, context.settingProvider.language!);
context.read<CopilotProvider>().sendText(text, context.settingProvider.language!, image);
image = null;
setState(() {});
_scrollToBottom();
}
@ -228,7 +233,7 @@ class _CopilotChatPageState extends State<CopilotChatPage> {
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<CopilotChatPage> {
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<CopilotProvider>(
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<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,
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<CopilotProvider>(
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<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