1239 lines
60 KiB
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),
|
|
),
|
|
);
|
|
}
|
|
}
|