didvan-app/lib/views/ai/ai_chat_page.dart

1239 lines
60 KiB
Dart

// ignore_for_file: library_private_types_in_public_api, deprecated_member_use, depend_on_referenced_packages, unnecessary_import
import 'package:didvan/views/ai/ai_state.dart';
import 'package:didvan/views/widgets/hoshan_app_bar.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/main.dart';
import 'package:didvan/models/ai/ai_chat_args.dart';
import 'package:didvan/models/ai/bots_model.dart';
import 'package:didvan/models/ai/chats_model.dart';
import 'package:didvan/models/ai/files_model.dart';
import 'package:didvan/models/ai/messages_model.dart';
import 'package:didvan/models/ai/ai_model_enum.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/utils/extension.dart';
import 'package:didvan/views/ai/ai_chat_state.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/ai/widgets/ai_message_bar.dart';
import 'package:didvan/views/ai/widgets/audio_wave.dart';
import 'package:didvan/views/ai/widgets/hoshan_drawer.dart';
import 'package:didvan/views/widgets/didvan/didvan_markdown.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/marquee_text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:didvan/views/widgets/video/chat_video_player.dart';
import 'package:didvan/views/widgets/video/custome_controls.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
class AiChatPage extends StatefulWidget {
final AiChatArgs args;
const AiChatPage({Key? key, required this.args}) : super(key: key);
@override
_AiChatPageState createState() => _AiChatPageState();
}
class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
final GlobalKey<ScaffoldState> scaffKey = GlobalKey<ScaffoldState>();
FocusNode focusNode = FocusNode();
BotsModel? _searchBot;
BotsModel? _geminiBot;
BotsModel? _grokBot;
bool _isSearchMode = false;
AiModel _selectedModel = AiModel.chatGPT;
late BotsModel _currentBot;
late AnimationController _fadeController;
late AnimationController _messageController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void didUpdateWidget(covariant AiChatPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.args.bot.id != oldWidget.args.bot.id) {
debugPrint("--- AI CHAT PAGE: Bot Updated ---");
debugPrint(
"Bot ID: ${widget.args.bot.id}, Bot Name: ${widget.args.bot.name}");
debugPrint("---------------------------------");
setState(() {
_currentBot = widget.args.bot;
_isSearchMode = _currentBot.id == 36;
});
final state = context.read<AiChatState>();
state.clearChat();
if (widget.args.bot.id == 35) {
state.fetchSuggestedQuestions();
}
if (widget.args.chat != null) {
state.chatId = widget.args.chat!.id!;
state.chat = widget.args.chat;
state.getAllMessages(state.chatId!);
}
if (widget.args.prompts != null) {
state.messages.add(MessageModel(
dateTime: DateTime.now()
.subtract(const Duration(minutes: 210))
.toIso8601String(),
prompts: [widget.args.prompts!]));
state.message.clear();
state.update();
state.postMessage(widget.args.bot, widget.args.assistantsName != null);
}
}
}
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_messageController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeIn,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _messageController,
curve: Curves.easeOutCubic,
));
_fadeController.forward();
_messageController.forward();
debugPrint("--- AI CHAT PAGE: Init ---");
debugPrint(
"Bot ID: ${widget.args.bot.id}, Bot Name: ${widget.args.bot.name}");
debugPrint("----------------------------");
_currentBot = widget.args.bot;
_isSearchMode = _currentBot.id == 36;
final state = context.read<AiChatState>();
if (widget.args.chat != null) {
state.chatId = widget.args.chat!.id!;
state.chat = widget.args.chat;
}
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
final bots = context.read<HistoryAiChatState>().bots;
if (mounted) {
setState(() {
_searchBot = bots.firstWhere((b) => b.id == 36);
try {
_geminiBot = bots.firstWhere((b) =>
b.id == 35 &&
(b.name?.toLowerCase().contains('gemini') ?? false));
} catch (e) {
_geminiBot = BotsModel(
id: 35,
name: 'Gemini',
responseType: 'text',
attachmentType: [
'audio',
'image',
'pdf',
'csv',
'doc',
'docx',
'xls',
'xlsx'
],
attachment: 1,
);
}
try {
_grokBot = bots.firstWhere((b) =>
b.id == 35 &&
(b.name?.toLowerCase().contains('grok') ?? false));
} catch (e) {
_grokBot = BotsModel(
id: 35,
name: 'Grok',
responseType: 'text',
attachmentType: [
'audio',
'image',
'pdf',
'csv',
'doc',
'docx',
'xls',
'xlsx'
],
attachment: 1,
);
}
});
}
} catch (e) {
if (mounted) {
setState(() {
_searchBot = BotsModel(
id: 36,
name: 'GPT-4o-mini-search',
responseType: 'text',
attachmentType: [
'audio',
'image',
'pdf',
'csv',
'doc',
'docx',
'xls',
'xlsx'
],
attachment: 1,
);
_geminiBot = BotsModel(
id: 35,
name: 'Gemini',
responseType: 'text',
attachmentType: [
'audio',
'image',
'pdf',
'csv',
'doc',
'docx',
'xls',
'xlsx'
],
attachment: 1,
);
_grokBot = BotsModel(
id: 35,
name: 'Grok',
responseType: 'text',
attachmentType: [
'audio',
'image',
'pdf',
'csv',
'doc',
'docx',
'xls',
'xlsx'
],
attachment: 1,
);
});
debugPrint(
"Search bot (ID 36) not found in History state: $e. Using fallback with audio support.");
}
}
if (widget.args.bot.id == 35) {
state.fetchSuggestedQuestions();
}
if (state.chatId != null) {
state.getAllMessages(state.chatId!).then((value) => Future.delayed(
const Duration(milliseconds: 100),
() {
focusNode.requestFocus();
if (state.messages.isNotEmpty) {
state.scrollController.animateTo(
state.scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
},
));
} else {
state.appState = AppState.idle;
Future.delayed(
const Duration(milliseconds: 100),
() => focusNode.requestFocus(),
);
}
if (widget.args.prompts != null) {
state.messages.add(MessageModel(
dateTime: DateTime.now()
.subtract(const Duration(minutes: 210))
.toIso8601String(),
prompts: [widget.args.prompts!]));
state.message.clear();
state.update();
await state.postMessage(
widget.args.bot, widget.args.assistantsName != null);
Future.delayed(const Duration(milliseconds: 100), () {
state.scrollController.animateTo(
state.scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
});
}
@override
void dispose() {
_fadeController.dispose();
_messageController.dispose();
super.dispose();
}
BotsModel _getSelectedBot() {
switch (_selectedModel) {
case AiModel.gemini:
return _geminiBot ?? _currentBot;
case AiModel.grok:
return _grokBot ?? _currentBot;
case AiModel.chatGPT:
default:
return _currentBot;
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (context.read<HistoryAiChatState>().refresh) {
context.read<HistoryAiChatState>().getChats();
context.read<HistoryAiChatState>().refresh = false;
}
context.read<AiState>().endChat();
await Future.delayed(Duration.zero);
return true;
},
child: Consumer<AiChatState>(
builder: (context, state, child) => Scaffold(
appBar: HoshanAppBar(
onBack: () async {
if (context.read<HistoryAiChatState>().refresh) {
context.read<HistoryAiChatState>().getChats();
context.read<HistoryAiChatState>().refresh = false;
}
context.read<AiState>().endChat();
await Future.delayed(const Duration(milliseconds: 50));
if (context.mounted) {
Navigator.pop(context);
}
},
withActions: false,
),
key: scaffKey,
resizeToAvoidBottomInset: true,
drawer: HoshanDrawer(
scaffKey: scaffKey,
),
body: StateHandler<AiChatState>(
state: state,
onRetry: () {
if (state.chatId != null) {
state.getAllMessages(state.chatId!);
}
if (widget.args.bot.id == 35) {
state.fetchSuggestedQuestions();
}
},
builder: (context, state) {
final userProvider = context.watch<UserProvider>();
return SingleChildScrollView(
reverse: true,
controller: state.scrollController,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (state.messages.isEmpty)
FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 90),
userProvider.isLoadingWelcome
? const SizedBox(height: 20)
: userProvider.welcomeMessage != null
? TweenAnimationBuilder<double>(
duration:
const Duration(milliseconds: 800),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.scale(
scale: 0.8 + (0.2 * value),
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.only(
bottom: 12.0,
left: 24,
right: 24),
child: Center(
child: DidvanText(
userProvider.welcomeMessage!,
textAlign: TextAlign.center,
fontSize: 18,
fontWeight: FontWeight.bold,
color: const Color.fromARGB(
255, 0, 126, 167),
),
),
),
)
: const SizedBox(height: 20),
TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 1000),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: Text(
'چطور می‌تونم کمکت کنم؟',
style: TextStyle(
color: DesignConfig.isDark
? const Color.fromARGB(255, 0, 90, 119)
: const Color.fromARGB(255, 0, 53, 70),
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 70),
if (widget.args.bot.id == 35)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0),
child: Row(
children: [
const SizedBox(width: 7),
SvgPicture.asset(
'lib/assets/icons/message-question.svg',
height: 25,
color: const Color.fromARGB(
255, 0, 126, 167),
),
const SizedBox(width: 7),
const DidvanText(
'سوالات پیشنهادی:',
fontSize: 16,
fontWeight: FontWeight.normal,
color: Color.fromARGB(
255, 0, 126, 167),
),
],
),
),
const SizedBox(height: 12),
state.isLoadingQuestions
? Center(
child: SpinKitThreeBounce(
color: Theme.of(context)
.colorScheme
.primary,
size: 18,
),
)
: state.suggestedQuestions.isEmpty
? Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 24.0),
child: Center(
child: DidvanText(
'سوالی برای پیشنهاد وجود ندارد.',
fontSize: 12,
color: Theme.of(context)
.colorScheme
.caption,
),
),
)
: ListView.builder(
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(),
itemCount: state
.suggestedQuestions.length,
itemBuilder: (context, index) {
final question =
state.suggestedQuestions[
index];
return TweenAnimationBuilder<
double>(
duration: Duration(
milliseconds: 400 +
(index * 100)),
tween: Tween(
begin: 0.0, end: 1.0),
curve: Curves.easeOut,
builder: (context, value,
child) {
return Opacity(
opacity: value,
child:
Transform.translate(
offset: Offset(
30 * (1 - value),
0),
child: child,
),
);
},
child: InkWell(
onTap: () {
if (state.messages
.isNotEmpty &&
DateTime.parse(state
.messages
.last
.dateTime)
.toPersianDateStr()
.contains(DateTime.parse(DateTime
.now()
.subtract(const Duration(
minutes:
210))
.toIso8601String())
.toPersianDateStr())) {
state.messages.last
.prompts
.add(Prompts(
error: false,
text: question,
fileLocal: null,
finished: true,
role: 'user',
createdAt: DateTime
.now()
.subtract(
const Duration(
minutes:
210))
.toIso8601String(),
));
} else {
state.messages.add(MessageModel(
dateTime: DateTime
.now()
.subtract(
const Duration(
minutes:
210))
.toIso8601String(),
prompts: [
Prompts(
error: false,
text:
question,
finished:
true,
fileLocal:
null,
role: 'user',
createdAt: DateTime
.now()
.subtract(const Duration(
minutes:
210))
.toIso8601String(),
)
]));
}
state.message.clear();
state.update();
state.postMessage(
_currentBot,
widget.args
.assistantsName !=
null);
Future.delayed(
const Duration(
milliseconds:
100), () {
state.scrollController
.animateTo(
state
.scrollController
.position
.maxScrollExtent,
duration:
const Duration(
milliseconds:
300),
curve:
Curves.easeOut,
);
});
},
child: Column(
children: [
Container(
margin:
const EdgeInsets
.symmetric(
horizontal:
24,
vertical:
4),
child: DidvanText(
question,
fontSize: 12,
color: const Color
.fromARGB(
255,
102,
102,
102),
),
),
const Padding(
padding: EdgeInsets
.fromLTRB(110,
5, 20, 5),
child: Divider(
height: 1,
color: Color
.fromARGB(
255,
210,
210,
210),
),
),
],
),
),
);
},
),
const SizedBox(height: 10),
],
)
else
const SizedBox(height: 20),
],
),
),
),
if (state.messages.isNotEmpty)
ListView.builder(
itemCount: state.messages.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, mIndex) {
final prompts = state.messages[mIndex].prompts;
final time = state.messages[mIndex].dateTime;
return Column(
children: [
timeLabel(context, time),
ListView.builder(
itemCount: prompts.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final message = prompts[index];
return messageBubble(
message, context, state, index, mIndex);
},
),
],
);
},
),
],
),
);
},
),
bottomNavigationBar: Padding(
padding: EdgeInsets.only(
bottom: _currentBot.id == 35
? MediaQuery.of(context).viewInsets.bottom
: (MediaQuery.of(context).viewInsets.bottom - 72.0)
.clamp(0.0, double.infinity),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AiMessageBar(
bot: _isSearchMode && _searchBot != null
? _searchBot!
: _getSelectedBot(),
attch: widget.args.attach,
assistantsName: widget.args.assistantsName,
showSearchToggle: _currentBot.id == 35 && _searchBot != null,
isSearchMode: _isSearchMode,
onSearchModeToggled: (bool isSearchOn) {
if (_searchBot == null) return;
setState(() {
_isSearchMode = isSearchOn;
});
},
showModelSelector: _currentBot.id == 35 &&
_geminiBot != null &&
_grokBot != null,
selectedModel: _selectedModel,
onModelChanged: (AiModel model) {
setState(() {
_selectedModel = model;
});
},
),
],
),
),
),
),
);
}
Center timeLabel(BuildContext context, String time) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.splash,
borderRadius: DesignConfig.lowBorderRadius,
),
child: DidvanText(
DateTime.parse(time).toPersianDateStr(),
style: Theme.of(context).textTheme.labelSmall,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.white
: Theme.of(context).colorScheme.black,
),
),
);
}
Widget messageBubble(Prompts message, BuildContext context, AiChatState state,
int index, int mIndex) {
FilesModel? file = message.fileLocal ??
(message.file == null
? null
: FilesModel(message.file.toString().replaceAll(' ', ''),
duration: message.duration != null
? Duration(seconds: message.duration!)
: null,
audio: message.audio,
video: message.video));
final isUser = message.role.toString().contains('user');
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 300 + (index * 50)),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(isUser ? -20 * (1 - value) : 20 * (1 - value), 0),
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: message.role.toString().contains('user')
? CrossAxisAlignment.start
: CrossAxisAlignment.end,
children: [
Row(
mainAxisAlignment: message.role.toString().contains('user')
? MainAxisAlignment.start
: MainAxisAlignment.end,
children: [
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width / 1.3),
decoration: BoxDecoration(
borderRadius: DesignConfig.mediumBorderRadius.copyWith(
bottomLeft: !message.role.toString().contains('user')
? Radius.zero
: null,
bottomRight: message.role.toString().contains('user')
? Radius.zero
: null,
),
color: message.error != null && message.error!
? Theme.of(context).colorScheme.error.withOpacity(0.4)
: (message.role.toString().contains('user')
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.focused)
.withOpacity(0.9),
border: Border.all(
color: Theme.of(context).colorScheme.border,
width: 0.5,
),
),
child: Container(
child: message.finished != null && !message.finished!
? Column(
children: [
ValueListenableBuilder<Stream<String>>(
valueListenable: state.messageOnstream,
builder: (context, value, child) {
return StreamBuilder<String>(
stream: value,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 16),
child: Directionality(
textDirection: snapshot.data
.toString()
.startsWithEnglish()
? TextDirection.ltr
: TextDirection.rtl,
child: DidvanMarkdownText(
text: "${snapshot.data}...",
),
),
);
},
);
},
),
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8.0),
child: SpinKitThreeBounce(
color: Theme.of(context).colorScheme.primary,
size: 18,
),
),
],
)
: Column(
children: [
if (file != null)
(file.isAudio())
? Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
child: AudioWave(
file: file.path,
totalDuration: file.duration,
),
)
: file.isVideo()
? Padding(
padding: const EdgeInsets.fromLTRB(
16, 16, 16, 0),
child: ClipRRect(
borderRadius:
DesignConfig.lowBorderRadius,
child: ChatVideoPlayer(
src: RequestHelper.baseUrl +
file.path,
custome: const CustomControls(),
),
),
)
: file.isImage()
? Padding(
padding:
const EdgeInsets.all(8.0),
child: messageImage(file),
)
: Padding(
padding:
const EdgeInsets.all(8.0),
child: messageFile(
context, message, state),
),
if (message.text != null &&
message.text!.isNotEmpty &&
((message.audio == null ||
(message.audio != null &&
!message.audio!))))
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 16),
child: Directionality(
textDirection: message.text
.toString()
.startsWithEnglish()
? TextDirection.ltr
: TextDirection.rtl,
child: DidvanMarkdownText(
text: message.text.toString(),
),
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (message.role
.toString()
.contains('user') &&
widget.args.assistantsName == null)
PopupMenuButton(
offset: const Offset(0, 46),
onSelected: (value) async {
navigatorKey.currentState!.pushNamed(
Routes.aiChat,
arguments: AiChatArgs(
bot: value,
prompts: message,
isTool: widget.args.isTool ??
context
.read<
HistoryAiChatState>()
.bots,
),
);
},
itemBuilder: (BuildContext context) {
final bots = widget.args.isTool ??
context
.read<HistoryAiChatState>()
.bots;
return <PopupMenuEntry>[
...List.generate(
bots.length,
(index) => PopupMenuItem(
value: bots[index],
height: 72,
child: Container(
constraints:
const BoxConstraints(
maxWidth: 200),
child: Row(
children: [
SkeletonImage(
imageUrl: bots[index]
.image
.toString(),
width: 42,
height: 42,
borderRadius:
BorderRadius
.circular(360),
),
const SizedBox(width: 12),
Expanded(
child: Directionality(
textDirection:
TextDirection.ltr,
child: DidvanText(
bots[index]
.name
.toString(),
maxLines: 1,
overflow:
TextOverflow
.ellipsis,
),
),
),
],
),
),
),
),
];
},
child: const SizedBox(),
),
if (message.role
.toString()
.contains('user') &&
index ==
state.messages[mIndex].prompts
.length -
2 &&
(_currentBot.editable != null &&
_currentBot.editable!))
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
state.isEdite = true;
state.message.text =
message.text.toString();
state.update();
},
child: Icon(
Icons.edit_outlined,
size: 18,
color: Theme.of(context)
.colorScheme
.focusedBorder,
),
),
),
if (message.file != null && kIsWeb)
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
final url =
'${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}';
MediaService.downloadFileFromWeb(
url);
},
child: Icon(
DidvanIcons.download_solid,
size: 18,
color: Theme.of(context)
.colorScheme
.focusedBorder,
),
),
),
if (message.file != null && !kIsWeb)
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
debugPrint(
"Download button tapped on iOS");
final url =
'${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}';
await MediaService.downloadFile(url,
name: message.fileName);
},
child: Icon(
DidvanIcons.download_solid,
size: 18,
color: Theme.of(context)
.colorScheme
.focusedBorder,
),
),
),
if (message.error != null && message.error!)
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
state.messages.last.prompts
.remove(message);
state.messages.last.prompts.add(
message.copyWith(error: false));
state.file = file;
state.update();
final botToUse = _isSearchMode &&
_searchBot != null
? _searchBot!
: _getSelectedBot();
await state.postMessage(
botToUse,
widget.args.assistantsName !=
null);
Future.delayed(
const Duration(
milliseconds: 100), () {
state.scrollController.animateTo(
state.scrollController.position
.maxScrollExtent,
duration: const Duration(
milliseconds: 300),
curve: Curves.easeOut,
);
});
},
child: Icon(
DidvanIcons.refresh_solid,
size: 18,
color: Theme.of(context)
.colorScheme
.focusedBorder,
),
),
),
if (message.text != null &&
message.text!.isNotEmpty &&
(file == null || !file.isImage()))
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
await Clipboard.setData(
ClipboardData(
text: state.messages[mIndex]
.prompts[index].text
.toString()));
Future.delayed(
Duration.zero,
() => ActionSheetUtils(context)
.showAlert(
AlertData(
message:
"متن با موفقیت کپی شد",
aLertType: ALertType.success,
),
),
);
},
child: Icon(
DidvanIcons.copy_regular,
size: 18,
color: Theme.of(context)
.colorScheme
.focusedBorder,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
if (message.id != null) {
state.deleteMessage(
message.id!, mIndex, index);
} else {
state.messages[mIndex].prompts
.removeAt(index);
state.update();
}
},
child: Icon(
DidvanIcons.trash_solid,
size: 18,
color: Theme.of(context)
.colorScheme
.focusedBorder,
),
),
),
],
),
),
const SizedBox(height: 8),
],
),
),
),
],
),
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
DidvanText(
DateTimeUtils.timeWithAmPm(message.createdAt.toString()),
style: Theme.of(context).textTheme.labelSmall,
color: Theme.of(context).colorScheme.caption,
),
],
),
],
),
),
);
}
Container messageFile(
BuildContext context, Prompts message, AiChatState state) {
final String fileName = message.fileName ?? message.fileLocal?.name ?? '';
return Container(
decoration: BoxDecoration(
borderRadius: DesignConfig.mediumBorderRadius,
color: Theme.of(context).colorScheme.border,
),
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
margin: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: Row(
children: [
const Icon(Icons.file_copy),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.sizeOf(context).width,
child: MarqueeText(
text: fileName,
style: const TextStyle(fontSize: 14),
stop: const Duration(seconds: 3),
textDirection: fileName.startsWithEnglish()
? TextDirection.ltr
: TextDirection.rtl,
),
),
],
),
),
],
),
);
}
Widget messageImage(FilesModel file) {
return GestureDetector(
onTap: () => ActionSheetUtils(context)
.openInteractiveViewer(context, file.path, !file.isNetwork()),
child: file.isNetwork()
? file.path.startsWith('blob:')
? ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.network(file.path),
)
: SkeletonImage(
pWidth: MediaQuery.sizeOf(context).width / 1,
pHeight: MediaQuery.sizeOf(context).height / 6,
imageUrl: file.path,
)
: ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.file(file.main),
),
);
}
}