|
|
|
|
@ -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,
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|