1382 lines
69 KiB
Dart
1382 lines
69 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;
|
|
}
|
|
}
|
|
|
|
String _getBotTitle() {
|
|
final name = _currentBot.name?.toLowerCase() ?? '';
|
|
if (name.contains('dall-e') ||
|
|
name.contains('dalle') ||
|
|
name.contains('image')) {
|
|
return 'ساخت عکس';
|
|
} else if (name.contains('veo') || name.contains('video')) {
|
|
return 'ساخت ویدیو';
|
|
} else if (name.contains('translate') ||
|
|
name.contains('translator') ||
|
|
name.contains('ترجمه')) {
|
|
return 'ترجمه';
|
|
} else if (name.contains('summarize') ||
|
|
name.contains('aisummery') ||
|
|
name.contains('خلاصه')) {
|
|
return 'خلاصهساز';
|
|
} else if (name.contains('speech') ||
|
|
name.contains('text-to-speech') ||
|
|
name.contains('aiaudio') ||
|
|
name.contains('متن به صوت')) {
|
|
return 'تبدیل متن به صوت';
|
|
} else if (name.contains('chart') ||
|
|
name.contains('graph') ||
|
|
name.contains('نمودار') ||
|
|
name.contains('تحلیل')) {
|
|
return 'تحلیل و ترسیم نمودار';
|
|
}
|
|
return _currentBot.short ?? _currentBot.name ?? 'هوش مصنوعی';
|
|
}
|
|
|
|
String _getBotSubtitle() {
|
|
final name = _currentBot.name?.toLowerCase() ?? '';
|
|
if (name.contains('dall-e') ||
|
|
name.contains('dalle') ||
|
|
name.contains('image')) {
|
|
return 'با استفاده از این بات، میتوانید تصاویر با کیفیت ایجاد کنید. پس از توصیف ایده خود در قابل چند عبارت، بات، تصویری نزدیک به درخواست شما تولید میکند. در صورت نیاز، میتوانید توضیح خود را اصلاح کرده و دوباره امتحان کنید تا به نتیجه دلخواه برسید.';
|
|
} else if (name.contains('veo') || name.contains('video')) {
|
|
return 'با استفاده از این بات، میتوانید ویدیوهای کوتاه و ساده را از ایدهها، متنها یا سناریوی موردنظر خود بسازید. کافی است توضیح دهید چه میخواهید، تا بات نسخه اولیه ویدیو را تولید کند. ';
|
|
} else if (name.contains('translate') ||
|
|
name.contains('translator') ||
|
|
name.contains('ترجمه')) {
|
|
return 'این بات به شما کمک میکند متنهای خود را بهصورت سریع و دقیق بین زبانهای فارسی و انگلیسی ترجمه کنید. تنها کافی است جمله یا پاراگراف خود را وارد کنید تا ترجمه روان و قابل اتکا دریافت کنید. ';
|
|
} else if (name.contains('summarize') ||
|
|
name.contains('aisummery') ||
|
|
name.contains('خلاصه')) {
|
|
return 'با استفاده از این بات، میتوانید هر متن طولانی را به نسخهای کوتاه، روان و دقیق تبدیل کنید. کافی است متن خود را ارسال کنید تا بات، مهمترین نکات آن را استخراج و در قالب خلاصهای قابلفهم ارائه کند. ';
|
|
} else if (name.contains('speech') ||
|
|
name.contains('text-to-speech') ||
|
|
name.contains('aiaudio') ||
|
|
name.contains('متن به صوت')) {
|
|
return 'این بات متن شما را با صدایی طبیعی و واضح به فایل صوتی تبدیل میکند. فقط کافی است متن موردنظر را وارد کنید تا نسخه صوتی آن را دریافت کنید. ';
|
|
} else if (name.contains('chart') ||
|
|
name.contains('graph') ||
|
|
name.contains('نمودار') ||
|
|
name.contains('تحلیل')) {
|
|
return 'این بات امکان تولید انواع نمودارها و ترسیمهای تحلیلی را برای شما فراهم میکند. تنها کافی است دادهها و نوع نمودار موردنظر را توضیح دهید تا خروجی تمیز و قابل استفاده دریافت کنید. اگر نتیجه مطابق نیازتان نبود، میتوانید دادهها یا سبک نمودار را اصلاح و دوباره تولید کنید.';
|
|
}
|
|
return _currentBot.description ?? '';
|
|
}
|
|
|
|
String? _getBotIcon() {
|
|
final name = _currentBot.name?.toLowerCase() ?? '';
|
|
if (name.contains('dall-e') ||
|
|
name.contains('dalle') ||
|
|
name.contains('image')) {
|
|
return 'lib/assets/icons/CrateImage.svg';
|
|
} else if (name.contains('veo') || name.contains('video')) {
|
|
return 'lib/assets/icons/VideoBot.svg';
|
|
} else if (name.contains('translate') ||
|
|
name.contains('translator') ||
|
|
name.contains('ترجمه')) {
|
|
return 'lib/assets/icons/TranslateBot.svg';
|
|
} else if (name.contains('summarize') ||
|
|
name.contains('aisummery') ||
|
|
name.contains('خلاصه')) {
|
|
return 'lib/assets/icons/SummeryBot.svg';
|
|
} else if (name.contains('speech') ||
|
|
name.contains('text-to-speech') ||
|
|
name.contains('aiaudio') ||
|
|
name.contains('متن به صوت')) {
|
|
return 'lib/assets/icons/TTV_Bot.svg';
|
|
} else if (name.contains('chart') ||
|
|
name.contains('graph') ||
|
|
name.contains('نمودار') ||
|
|
name.contains('تحلیل')) {
|
|
return 'lib/assets/icons/ChartBot.svg';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@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),
|
|
if (widget.args.bot.id == 35)
|
|
TweenAnimationBuilder<double>(
|
|
duration: const Duration(milliseconds: 1000),
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
curve: Curves.easeOut,
|
|
builder: (context, value, child) {
|
|
return Center(
|
|
child: Opacity(
|
|
opacity: value,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 20 * (1 - value)),
|
|
child: child,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: Center(
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (widget.args.bot.id != 35)
|
|
TweenAnimationBuilder<double>(
|
|
duration: const Duration(milliseconds: 1000),
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
curve: Curves.easeOut,
|
|
builder: (context, value, child) {
|
|
return Center(
|
|
child: Opacity(
|
|
opacity: value,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 20 * (1 - value)),
|
|
child: child,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24.0),
|
|
child: Column(
|
|
children: [
|
|
if (_getBotIcon() != null)
|
|
const SizedBox(
|
|
height: 10,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 16.0),
|
|
child: SvgPicture.asset(
|
|
_getBotIcon()!,
|
|
height: 70,
|
|
),
|
|
),
|
|
DidvanText(
|
|
_getBotTitle(),
|
|
textAlign: TextAlign.center,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: const Color.fromARGB(
|
|
255, 27, 60, 89),
|
|
),
|
|
const SizedBox(height: 12),
|
|
DidvanText(
|
|
_getBotSubtitle(),
|
|
textAlign: TextAlign.start,
|
|
fontSize: 14,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.caption,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
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(
|
|
monthString:
|
|
'')
|
|
.contains(DateTime.parse(DateTime
|
|
.now()
|
|
.subtract(const Duration(
|
|
minutes:
|
|
210))
|
|
.toIso8601String())
|
|
.toPersianDateStr(
|
|
monthString:
|
|
''))) {
|
|
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(monthString: ''),
|
|
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.highBorderRadius.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.focused
|
|
: Theme.of(context).colorScheme.surface)
|
|
.withOpacity(0.9),
|
|
border: Border.all(
|
|
color: message.role.toString().contains('user')
|
|
? const Color.fromARGB(0, 0, 0, 0) :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: SvgPicture.asset(
|
|
'lib/assets/icons/edit-2.svg',
|
|
height: 20,
|
|
)),
|
|
),
|
|
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: SvgPicture.asset(
|
|
'lib/assets/icons/material-symbols_download-rounded.svg',
|
|
height: 20,
|
|
)),
|
|
),
|
|
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: SvgPicture.asset(
|
|
'lib/assets/icons/refresh-2.svg',
|
|
height: 20,
|
|
)),
|
|
),
|
|
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: SvgPicture.asset(
|
|
'lib/assets/icons/copy.svg',
|
|
height: 20,
|
|
)),
|
|
),
|
|
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: SvgPicture.asset(
|
|
'lib/assets/icons/trash.svg',
|
|
height: 20,
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
DidvanText(
|
|
DateTimeUtils.timeWithAmPm(message.createdAt.toString())
|
|
.toPersianDigit(),
|
|
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),
|
|
),
|
|
);
|
|
}
|
|
}
|