diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 4401ef0..43fc3c0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@
+
-
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- didvan
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- FirebaseAppDelegateProxyEnabled
-
- LSRequiresIPhoneOS
-
- NSCameraUsageDescription
- We need to access to the user gallery to add user profile photo
- NSMicrophoneUsageDescription
- We need to access to the microphone to record audio file
- NSPhotoLibraryUsageDescription
- We need to access to the user gallery to add user profile photo
- UIApplicationSupportsIndirectInputEvents
-
- UIBackgroundModes
-
- audio
- fetch
- remote-notification
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
- UIInterfaceOrientationPortrait
-
- UIViewControllerBasedStatusBarAppearance
-
- UNNotificationServiceExtension
-
- $(PRODUCT_BUNDLE_IDENTIFIER).MyNotificationServiceExtension
-
-
-
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ didvan
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ FirebaseAppDelegateProxyEnabled
+
+ LSRequiresIPhoneOS
+
+ NSCameraUsageDescription
+ We need to access to the user gallery to add user profile photo
+ NSMicrophoneUsageDescription
+ We need to access to the microphone to record audio file
+ NSPhotoLibraryUsageDescription
+ We need to access to the user gallery to add user profile photo
+ UIApplicationSupportsIndirectInputEvents
+
+ UIBackgroundModes
+
+ audio
+ fetch
+ remote-notification
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+ UIInterfaceOrientationPortrait
+
+ UIViewControllerBasedStatusBarAppearance
+
+ UNNotificationServiceExtension
+
+ $(PRODUCT_BUNDLE_IDENTIFIER).MyNotificationServiceExtension
+
+ NSMicrophoneUsageDescription
+ Some message to describe why you need this permission
+
+
\ No newline at end of file
diff --git a/lib/models/ai/ai_chat_args.dart b/lib/models/ai/ai_chat_args.dart
index 0caf703..d0ab3cf 100644
--- a/lib/models/ai/ai_chat_args.dart
+++ b/lib/models/ai/ai_chat_args.dart
@@ -1,8 +1,9 @@
import 'package:didvan/models/ai/bots_model.dart';
+import 'package:didvan/models/ai/chats_model.dart';
class AiChatArgs {
final BotsModel bot;
- final int? chatId;
+ final ChatsModel? chat;
- AiChatArgs({required this.bot, this.chatId});
+ AiChatArgs({required this.bot, this.chat});
}
diff --git a/lib/models/ai/chats_model.dart b/lib/models/ai/chats_model.dart
index 02e71a1..7c07d41 100644
--- a/lib/models/ai/chats_model.dart
+++ b/lib/models/ai/chats_model.dart
@@ -10,17 +10,20 @@ class ChatsModel {
String? updatedAt;
BotsModel? bot;
List? prompts;
+ bool? isEditing;
- ChatsModel(
- {this.id,
- this.userId,
- this.botId,
- this.title,
- this.placeholder,
- this.createdAt,
- this.updatedAt,
- this.bot,
- this.prompts});
+ ChatsModel({
+ this.id,
+ this.userId,
+ this.botId,
+ this.title,
+ this.placeholder,
+ this.createdAt,
+ this.updatedAt,
+ this.bot,
+ this.prompts,
+ this.isEditing = false,
+ });
ChatsModel.fromJson(Map json) {
id = json['id'];
@@ -56,6 +59,31 @@ class ChatsModel {
}
return data;
}
+
+ ChatsModel copyWith(
+ {int? id,
+ int? userId,
+ int? botId,
+ String? title,
+ String? placeholder,
+ String? createdAt,
+ String? updatedAt,
+ BotsModel? bot,
+ List? prompts,
+ bool? isEditing}) {
+ return ChatsModel(
+ id: id ?? this.id,
+ userId: userId ?? this.userId,
+ botId: botId ?? this.botId,
+ title: title ?? this.title,
+ placeholder: placeholder ?? this.placeholder,
+ bot: bot ?? this.bot,
+ createdAt: createdAt ?? this.createdAt,
+ updatedAt: updatedAt ?? this.updatedAt,
+ prompts: prompts ?? this.prompts,
+ isEditing: isEditing ?? this.isEditing,
+ );
+ }
}
class Prompts {
@@ -67,16 +95,19 @@ class Prompts {
String? role;
String? createdAt;
bool? finished;
+ bool? error;
- Prompts(
- {this.id,
- this.chatId,
- this.text,
- this.file,
- this.fileName,
- this.role,
- this.createdAt,
- this.finished});
+ Prompts({
+ this.id,
+ this.chatId,
+ this.text,
+ this.file,
+ this.fileName,
+ this.role,
+ this.createdAt,
+ this.finished,
+ this.error,
+ });
Prompts.fromJson(Map json) {
id = json['id'];
@@ -100,16 +131,16 @@ class Prompts {
return data;
}
- Prompts copyWith({
- int? id,
- int? chatId,
- String? text,
- String? file,
- String? fileName,
- String? role,
- String? createdAt,
- bool? finished,
- }) {
+ Prompts copyWith(
+ {int? id,
+ int? chatId,
+ String? text,
+ String? file,
+ String? fileName,
+ String? role,
+ String? createdAt,
+ bool? finished,
+ bool? error}) {
return Prompts(
id: id ?? this.id,
chatId: chatId ?? this.chatId,
@@ -119,6 +150,7 @@ class Prompts {
role: role ?? this.role,
createdAt: createdAt ?? this.createdAt,
finished: finished ?? this.finished,
+ error: error ?? this.error,
);
}
}
diff --git a/lib/models/ai/files_model.dart b/lib/models/ai/files_model.dart
new file mode 100644
index 0000000..d5c54ea
--- /dev/null
+++ b/lib/models/ai/files_model.dart
@@ -0,0 +1,16 @@
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+class FilesModel {
+ final String path;
+ late String basename;
+ late String extname;
+ late File main;
+
+ FilesModel(this.path) {
+ basename = p.basename(path);
+ extname = p.extension(path);
+ main = File(path);
+ }
+}
diff --git a/lib/models/view/action_sheet_data.dart b/lib/models/view/action_sheet_data.dart
index f1b533d..e463af9 100644
--- a/lib/models/view/action_sheet_data.dart
+++ b/lib/models/view/action_sheet_data.dart
@@ -12,6 +12,7 @@ class ActionSheetData {
final Color? titleColor;
final bool hasDismissButton;
final bool hasConfirmButton;
+ final bool hasConfirmButtonClose;
final bool withoutButtonMode;
final bool smallDismissButton;
final bool isBackgroundDropBlur;
@@ -32,6 +33,7 @@ class ActionSheetData {
this.smallDismissButton = false,
this.withoutButtonMode = false,
this.isBackgroundDropBlur = false,
+ this.hasConfirmButtonClose = true,
this.backgroundColor,
});
}
diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart
index 6ded8a5..b23d322 100644
--- a/lib/routes/route_generator.dart
+++ b/lib/routes/route_generator.dart
@@ -306,6 +306,10 @@ class RouteGenerator {
child: AiChatPage(
args: settings.arguments as AiChatArgs,
)));
+ case Routes.aiHistory:
+ return _createRoute(HistoryAiChatPage(
+ archived: settings.arguments as bool?,
+ ));
default:
return _errorRoute(settings.name ?? '');
}
diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart
index fd655d5..57e58ba 100644
--- a/lib/routes/routes.dart
+++ b/lib/routes/routes.dart
@@ -1,6 +1,8 @@
class Routes {
static const String splash = '/';
static const String aiChat = '/ai-chat';
+ static const String aiHistory = '/ai-history';
+
static const String home = '/home';
static const String radars = '/radars';
static const String news = '/news';
diff --git a/lib/services/ai/ai_api_service.dart b/lib/services/ai/ai_api_service.dart
index 47508a8..5ccc09f 100644
--- a/lib/services/ai/ai_api_service.dart
+++ b/lib/services/ai/ai_api_service.dart
@@ -1,17 +1,17 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
import 'package:didvan/services/storage/storage.dart';
import 'package:http/http.dart' as http;
-import 'package:http/http.dart';
+import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
import 'package:http_parser/http_parser.dart' as parser;
class AiApiService {
static const String baseUrl = 'https://api.didvan.app/ai';
- static final _client = http.Client();
static Future initial(
{required final String url,
@@ -33,25 +33,40 @@ class AiApiService {
if (file != null) {
final length = await file.length();
String basename = p.basename(file.path);
- String mediaExtension = p.extension(file.path).replaceAll('.', '');
- String mediaFormat = 'image';
- if (mediaExtension.contains('pdf')) {
- mediaFormat = 'application';
+ String? mimeType =
+ lookupMimeType(file.path); // Use MIME type instead of file extension
+ mimeType ??= 'application/octet-stream';
+ if (mimeType.startsWith('audio')) {
+ mimeType = 'audio/${p.extension(file.path).replaceAll('.', '')}';
}
request.files.add(
http.MultipartFile('file', file.readAsBytes().asStream(), length,
filename: basename,
- contentType: parser.MediaType(mediaFormat, mediaExtension)),
+ contentType: parser.MediaType.parse(
+ mimeType)), // Use MediaType.parse to parse the MIME type
);
}
return request;
}
- static Future getResponse(http.MultipartRequest request) async {
- final res = _client.send(request).timeout(
- const Duration(seconds: 30),
- );
- final http.StreamedResponse response = await res;
- return response.stream;
+ static Future>> getResponse(
+ http.MultipartRequest req) async {
+ try {
+ final response = await http.Client().send(req);
+ if (response.statusCode == 400) {
+ // Handle 400 response
+ final errorResponse = await response.stream.bytesToString();
+ final errorJson = jsonDecode(errorResponse);
+ throw Exception(errorJson['error'] ?? 'Bad Request');
+ } else if (response.statusCode != 200) {
+ // Handle other non-200 responses
+ throw Exception('Failed to load data');
+ } else {
+ return response.stream;
+ }
+ } catch (e) {
+ // Handle any other errors
+ throw Exception('Failed to load data');
+ }
}
}
diff --git a/lib/services/app_initalizer.dart b/lib/services/app_initalizer.dart
index e65d2f8..dc04a4a 100644
--- a/lib/services/app_initalizer.dart
+++ b/lib/services/app_initalizer.dart
@@ -1,4 +1,3 @@
-
import 'package:didvan/main.dart';
import 'package:didvan/models/notification_message.dart';
import 'package:didvan/models/requests/news.dart';
@@ -171,4 +170,24 @@ class AppInitializer {
int id = int.parse('${data.userId}${data.id}$t');
return id;
}
+
+ static Map messagesData(String dataMessgae) {
+ dataMessgae = dataMessgae.replaceAll('{{{', '');
+ dataMessgae = dataMessgae.replaceAll('}}}', '');
+ final pairs = dataMessgae.substring(1, dataMessgae.length - 1).split(',');
+
+ // Create a map to store the key-value pairs
+ final data = {};
+ // Iterate over the key-value pairs and add them to the map
+ for (final pair in pairs) {
+ final keyValue = pair.split(':');
+ if (keyValue.length == 2) {
+ final key = keyValue[0].trim().replaceAll('"', '');
+ final value = keyValue[1].trim().replaceAll('"', '');
+ data[key] = int.parse(value.replaceAll(' ', ''));
+ }
+ }
+
+ return data;
+ }
}
diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart
index fcb9f96..c7d2d08 100644
--- a/lib/services/media/media.dart
+++ b/lib/services/media/media.dart
@@ -109,4 +109,11 @@ class MediaService {
);
return result;
}
+
+ static Future pickAudioFile() async {
+ return await FilePicker.platform.pickFiles(
+ type: FileType.audio,
+ allowMultiple: false,
+ );
+ }
}
diff --git a/lib/services/network/request.dart b/lib/services/network/request.dart
index 89a4e64..6f60897 100644
--- a/lib/services/network/request.dart
+++ b/lib/services/network/request.dart
@@ -66,7 +66,7 @@ class RequestService {
headers: _headers,
)
.timeout(
- const Duration(seconds: 100),
+ const Duration(seconds: 30),
)
.catchError(
(e) => throw e,
@@ -86,7 +86,7 @@ class RequestService {
headers: _headers,
)
.timeout(
- const Duration(seconds: 100),
+ const Duration(seconds: 30),
)
.catchError(
(e) {
@@ -108,7 +108,7 @@ class RequestService {
headers: _headers,
)
.timeout(
- const Duration(seconds: 100),
+ const Duration(seconds: 30),
)
.catchError(
(e) => throw e,
@@ -149,7 +149,7 @@ class RequestService {
final streamedResponse = await request
.send()
.timeout(
- const Duration(seconds: 100),
+ const Duration(seconds: 30),
)
.catchError(
(e) => throw e,
@@ -170,7 +170,7 @@ class RequestService {
headers: _headers,
)
.timeout(
- const Duration(seconds: 100),
+ const Duration(seconds: 30),
)
.catchError(
(e) => throw e,
diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart
index 39c2d71..b447b17 100644
--- a/lib/services/network/request_helper.dart
+++ b/lib/services/network/request_helper.dart
@@ -201,14 +201,33 @@ class RequestHelper {
static String reportComment(int id) => '$baseUrl/comment/$id/report';
static String widgetNews() => '$baseUrl/user/widget';
static String aiChats() => '$baseUrl/ai/chat';
+ static String aiArchived() => '$baseUrl/ai/chat${_urlConcatGenerator([
+ const MapEntry('archived', true),
+ ])}';
static String aiBots() => '$baseUrl/ai/bot';
static String aiSearchBots(String q) =>
'$baseUrl/ai/bot${_urlConcatGenerator([
MapEntry('q', q),
])}';
+ static String aiSearchChats(String q) =>
+ '$baseUrl/ai/chat${_urlConcatGenerator([
+ MapEntry('q', q),
+ ])}';
+ static String aiSearchArchived(String q) =>
+ '$baseUrl/ai/chat${_urlConcatGenerator([
+ MapEntry('q', q),
+ const MapEntry('archived', true),
+ ])}';
static String aiAChat(int id) => '$baseUrl/ai/chat/$id';
static String aiChatId() => '$baseUrl/ai/chat/id';
static String aiDeleteChats() => '$baseUrl/ai/chat';
+ static String aiChangeChats(int id) => '$baseUrl/ai/chat/$id/title';
+ static String deleteChat(int id) => '$baseUrl/ai/chat/$id';
+ static String deleteMessage(int chatId, int messageId) =>
+ '$baseUrl/ai/chat/$chatId/message/$messageId';
+ static String deleteAllChats() => '$baseUrl/ai/chat/all';
+ static String archivedChat(int id) => '$baseUrl/ai/chat/$id/archive';
+ static String placeholder(int id) => '$baseUrl/ai/chat/$id/placeholder';
static String _urlConcatGenerator(List> additions) {
String result = '';
diff --git a/lib/utils/action_sheet.dart b/lib/utils/action_sheet.dart
index 602ebfc..cd0f7a0 100644
--- a/lib/utils/action_sheet.dart
+++ b/lib/utils/action_sheet.dart
@@ -2,14 +2,17 @@ import 'dart:async';
import 'dart:ui';
import 'package:bot_toast/bot_toast.dart';
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/models/view/alert_data.dart';
+import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
+import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -253,8 +256,10 @@ class ActionSheetUtils {
Expanded(
child: DidvanButton(
onPressed: () {
- pop();
data.onConfirmed?.call();
+ if (data.hasConfirmButtonClose) {
+ pop();
+ }
},
title: data.confrimTitle ?? 'تایید',
),
@@ -269,6 +274,117 @@ class ActionSheetUtils {
);
}
+ static Future botsDialogSelect(
+ {required final BuildContext context,
+ required final HistoryAiChatState state}) async {
+ ActionSheetUtils.context = context;
+ ActionSheetUtils.openDialog(
+ data: ActionSheetData(
+ hasConfirmButton: false,
+ hasDismissButton: false,
+ content: Column(
+ children: [
+ // Row(
+ // mainAxisAlignment: MainAxisAlignment.end,
+ // children: [
+ // Padding(
+ // padding: const EdgeInsets.symmetric(vertical: 8.0),
+ // child: InkWell(
+ // onTap: () {
+ // ActionSheetUtils.pop();
+ // },
+ // child: const Icon(DidvanIcons.close_solid)),
+ // )
+ // ],
+ // ),
+ // SearchField(
+ // title: 'هوش مصنوعی',
+ // value: state.search,
+ // onChanged: (value) {
+ // state.search = value;
+ // if (value.isEmpty) {
+ // state.getBots();
+ // return;
+ // }
+ // state.timer?.cancel();
+ // state.timer = Timer(const Duration(seconds: 1), () {
+ // state.getSearchBots(value);
+ // });
+ // },
+ // focusNode: FocusNode()),
+ // const SizedBox(
+ // height: 12,
+ // ),
+ SizedBox(
+ width: double.infinity,
+ height: MediaQuery.sizeOf(context).height / 3,
+ child: ValueListenableBuilder(
+ valueListenable: state.loadingBots,
+ builder: (context, value, child) => value
+ ? Center(
+ child: Image.asset(
+ Assets.loadingAnimation,
+ width: 60,
+ height: 60,
+ ),
+ )
+ : state.bots.isEmpty
+ ? Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12.0),
+ child: EmptyState(
+ asset: Assets.emptyResult,
+ title: 'نتیجهای پیدا نشد',
+ height: 120,
+ ),
+ )
+ : ListView.builder(
+ padding:
+ const EdgeInsets.symmetric(vertical: 12),
+ itemCount: state.bots.length,
+ physics: const BouncingScrollPhysics(),
+ shrinkWrap: true,
+ itemBuilder: (context, index) {
+ final bot = state.bots[index];
+ return InkWell(
+ onTap: () {
+ ActionSheetUtils.pop();
+ state.bot = bot;
+ state.update();
+ },
+ child: Container(
+ alignment: Alignment.center,
+ padding: const EdgeInsets.symmetric(
+ vertical: 8),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: Theme.of(context)
+ .colorScheme
+ .border,
+ width: 1))),
+ child: Row(
+ children: [
+ ClipOval(
+ child: CachedNetworkImage(
+ imageUrl: bot.image.toString(),
+ width: 42,
+ height: 42,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Text(bot.name.toString())
+ ],
+ ),
+ ),
+ );
+ }),
+ ),
+ )
+ ],
+ )));
+ }
+
static void pop() {
DesignConfig.updateSystemUiOverlayStyle();
Navigator.of(context).pop();
diff --git a/lib/views/ai/ai.dart b/lib/views/ai/ai.dart
index 8b017c1..61d7afe 100644
--- a/lib/views/ai/ai.dart
+++ b/lib/views/ai/ai.dart
@@ -1,6 +1,18 @@
// ignore_for_file: library_private_types_in_public_api
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:didvan/config/design_config.dart';
+import 'package:didvan/config/theme_data.dart';
+import 'package:didvan/constants/app_icons.dart';
+import 'package:didvan/constants/assets.dart';
+import 'package:didvan/models/ai/ai_chat_args.dart';
+import 'package:didvan/routes/routes.dart';
+import 'package:didvan/utils/action_sheet.dart';
+import 'package:didvan/views/ai/history_ai_chat_state.dart';
+import 'package:didvan/views/home/home.dart';
+import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
class Ai extends StatefulWidget {
const Ai({Key? key}) : super(key: key);
@@ -10,8 +22,162 @@ class Ai extends StatefulWidget {
}
class _AiState extends State {
+ @override
+ void initState() {
+ final state = context.read();
+ Future.delayed(
+ Duration.zero,
+ () {
+ // state.getChats();
+ state.getBots();
+ },
+ );
+
+ super.initState();
+ }
+
@override
Widget build(BuildContext context) {
- return Container();
+ return Consumer(
+ builder: (context, state, child) {
+ if (state.bots.isEmpty) {
+ return Center(
+ child: Image.asset(
+ Assets.loadingAnimation,
+ width: 60,
+ height: 60,
+ ),
+ );
+ }
+ final bot = state.bot!;
+ return Stack(
+ children: [
+ Column(
+ children: [
+ const SizedBox(
+ height: 12,
+ ),
+ Icon(
+ DidvanIcons.ai_solid,
+ size: MediaQuery.sizeOf(context).width / 5,
+ ),
+ const DidvanText('هوشان'),
+ const SizedBox(
+ height: 24,
+ ),
+ InkWell(
+ onTap: () => ActionSheetUtils.botsDialogSelect(
+ context: context, state: state),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(DidvanIcons.caret_down_solid),
+ Text(bot.name.toString()),
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 8,
+ ),
+ Container(
+ width: MediaQuery.sizeOf(context).height / 5,
+ height: MediaQuery.sizeOf(context).height / 5,
+ decoration: BoxDecoration(
+ borderRadius: DesignConfig.highBorderRadius,
+ color: Theme.of(context).colorScheme.focused),
+ padding: const EdgeInsets.all(12),
+ child: CachedNetworkImage(
+ imageUrl: bot.image.toString(),
+ ),
+ ),
+ const SizedBox(
+ height: 24,
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20.0),
+ child: Text(
+ "به هوشان, هوش مصنوعی دیدوان خوش آمدید. \nبرای شروع گفتگو پیام مورد نظر خود را در کادر زیر بنویسید.\n دریافت پاسخ از: ${bot.name}",
+ textAlign: TextAlign.center,
+ ),
+ )
+ ],
+ ),
+ Positioned(
+ bottom: 32,
+ left: 20,
+ right: 20,
+ child: InkWell(
+ onTap: () => Navigator.of(context).pushNamed(Routes.aiChat,
+ arguments: AiChatArgs(
+ bot: bot,
+ )),
+ child: Container(
+ decoration: BoxDecoration(
+ boxShadow: DesignConfig.defaultShadow,
+ color: Theme.of(context).colorScheme.white,
+ borderRadius: BorderRadius.circular(360)),
+ child: Row(
+ children: [
+ const SizedBox(
+ width: 8,
+ ),
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Theme.of(context).colorScheme.border),
+ child: const Icon(
+ DidvanIcons.mic_regular,
+ ),
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ ),
+ child: Form(
+ child: TextFormField(
+ textInputAction: TextInputAction.newline,
+ style: Theme.of(context).textTheme.bodyMedium,
+ minLines: 1,
+ enabled: false,
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ hintText: 'بنویسید...',
+ hintStyle: Theme.of(context)
+ .textTheme
+ .bodySmall!
+ .copyWith(
+ color: Theme.of(context)
+ .colorScheme
+ .disabledText),
+ ),
+ ))))
+ ],
+ ),
+ )),
+ ),
+ Positioned(
+ top: 32,
+ right: 0,
+ child: InkWell(
+ onTap: () => homeScaffKey.currentState!.openDrawer(),
+ child: Container(
+ width: 46,
+ height: 46,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.white,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(12),
+ bottomLeft: Radius.circular(12)),
+ boxShadow: DesignConfig.defaultShadow),
+ child: const Icon(DidvanIcons.angle_left_light),
+ )),
+ )
+ ],
+ );
+ },
+ );
}
}
diff --git a/lib/views/ai/ai_chat_page.dart b/lib/views/ai/ai_chat_page.dart
index aa38c53..16b85b4 100644
--- a/lib/views/ai/ai_chat_page.dart
+++ b/lib/views/ai/ai_chat_page.dart
@@ -1,39 +1,32 @@
// ignore_for_file: library_private_types_in_public_api, deprecated_member_use, depend_on_referenced_packages
-import 'dart:io';
-
import 'package:cached_network_image/cached_network_image.dart';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/constants/assets.dart';
-import 'package:didvan/main.dart';
import 'package:didvan/models/ai/ai_chat_args.dart';
import 'package:didvan/models/ai/chats_model.dart';
-import 'package:didvan/models/ai/messages_model.dart';
+import 'package:didvan/models/ai/files_model.dart';
import 'package:didvan/models/enums.dart';
+import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/models/view/alert_data.dart';
-import 'package:didvan/services/media/media.dart';
+import 'package:didvan/services/storage/storage.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/ai/ai_chat_state.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
-import 'package:didvan/views/widgets/animated_visibility.dart';
+import 'package:didvan/views/ai/widgets/ai_message_bar.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
-import 'package:didvan/views/widgets/marquee_text.dart';
-import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
-import 'package:file_picker/file_picker.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
-import 'package:image_cropper/image_cropper.dart';
-import 'package:image_picker/image_picker.dart';
+import 'package:mime/mime.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
-import 'package:path/path.dart' as p;
+import 'package:voice_message_package/voice_message_package.dart';
class AiChatPage extends StatefulWidget {
final AiChatArgs args;
@@ -44,15 +37,32 @@ class AiChatPage extends StatefulWidget {
}
class _AiChatPageState extends State {
- TextEditingController message = TextEditingController();
-
+ FocusNode focusNode = FocusNode();
@override
void initState() {
final state = context.read();
- state.chatId = widget.args.chatId;
- if (state.chatId != null) {
- state.getAllMessages(state.chatId!);
+ if (widget.args.chat != null) {
+ state.chatId = widget.args.chat!.id!;
+ state.chat = widget.args.chat;
}
+
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
+ if (state.chatId != null) {
+ state.getAllMessages(state.chatId!).then((value) => Future.delayed(
+ const Duration(
+ milliseconds: 100,
+ ),
+ () => focusNode.requestFocus(),
+ ));
+ } else {
+ Future.delayed(
+ const Duration(
+ milliseconds: 100,
+ ),
+ () => focusNode.requestFocus(),
+ );
+ }
+ });
super.initState();
}
@@ -63,372 +73,216 @@ class _AiChatPageState extends State {
context.read().getChats();
return true;
},
- child: Scaffold(
- appBar: AppBar(
- shadowColor: Theme.of(context).colorScheme.border,
- title: Row(
- children: [
- Padding(
- padding: const EdgeInsets.all(12.0),
- child: ClipOval(
- child: CachedNetworkImage(
- width: 32,
- height: 32,
- imageUrl: widget.args.bot.image.toString(),
- ),
- ),
- ),
- Text('چت با ${widget.args.bot.name}'),
- ],
- ),
- automaticallyImplyLeading: false,
- actions: [
- DidvanIconButton(
- icon: DidvanIcons.angle_left_regular,
- onPressed: () {
- context.read().getChats();
- navigatorKey.currentState!.pop();
- },
- )
- ],
- ),
- body: Consumer(
- builder: (BuildContext context, AiChatState state, Widget? child) =>
- state.loading
- ? Center(
- child: Image.asset(
- Assets.loadingAnimation,
- width: 60,
- height: 60,
- ),
- )
- : state.messages.isEmpty
- ? Center(
- child: EmptyState(
- asset: Assets.emptyChat,
- title: 'اولین پیام را بنویسید...',
- ),
- )
- : SingleChildScrollView(
- reverse: true,
- controller: state.scrollController,
- child: ListView.builder(
- itemCount: state.messages.length,
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- padding: const EdgeInsets.only(bottom: 90),
- 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);
- },
- ),
- ],
- );
- }),
- ),
- ),
- bottomSheet: Consumer(
- builder: (BuildContext context, AiChatState state, Widget? child) {
- return Container(
- width: MediaQuery.sizeOf(context).width,
- padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
- decoration: BoxDecoration(
- border: Border(
- top: BorderSide(
- color: Theme.of(context).colorScheme.cardBorder,
- ),
- ),
- color: Theme.of(context).colorScheme.surface,
- ),
- child: Column(
- mainAxisSize: MainAxisSize.min,
+ child: Consumer(
+ builder: (context, state, child) => Scaffold(
+ appBar: AppBar(
+ shadowColor: Theme.of(context).colorScheme.border,
+ title: Text(widget.args.bot.name.toString()),
+ leading: Row(
children: [
- if (widget.args.bot.attachment! == 2) fileContainer(context),
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- const SizedBox(
- width: 12,
- ),
- SizedBox(
- width: 46,
- height: 46,
- child: Center(
- child: state.onResponsing
- ? Center(
- child: SpinKitThreeBounce(
- size: 18,
- color: Theme.of(context)
- .colorScheme
- .focusedBorder,
- ),
- )
- : DidvanIconButton(
- icon: DidvanIcons.send_solid,
- size: 32,
- color: Theme.of(context)
- .colorScheme
- .focusedBorder,
- onPressed: () async {
- if (state.file == null &&
- message.text.isEmpty) {
- return;
- }
-
- 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(
- text: message.text,
- file: p.basename(state.file!.path),
- fileName: p.basename(state.file!.path),
- 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(
- text: message.text,
- finished: true,
- file:
- p.basename(state.file!.path),
- fileName:
- p.basename(state.file!.path),
- role: 'user',
- createdAt: DateTime.now()
- .subtract(const Duration(
- minutes: 210))
- .toIso8601String(),
- )
- ]));
- }
-
- await state.postMessage(widget.args.bot);
- message.clear();
- state.file = null;
- },
- ),
- ),
- ),
- const SizedBox(
- width: 12,
- ),
- Expanded(
- child: Form(
- child: widget.args.bot.attachment! != 1
- ? TextFormField(
- textInputAction: TextInputAction.newline,
- style: Theme.of(context).textTheme.bodyMedium,
- maxLines: 6,
- minLines: 1,
- // keyboardType: TextInputType.text,
- controller: message,
- enabled: !state.onResponsing,
- decoration: InputDecoration(
- border: InputBorder.none,
- hintText: 'بنویسید...',
- hintStyle: Theme.of(context)
- .textTheme
- .bodySmall!
- .copyWith(
- color: Theme.of(context)
- .colorScheme
- .disabledText),
- ),
- onChanged: (value) {},
- )
- : fileContainer(context),
- ),
- ),
- const SizedBox(
- width: 12,
- ),
- if (widget.args.bot.attachment! != 0)
- SizedBox(
- width: 46,
- height: 46,
- child: Center(
- child: PopupMenuButton(
- onSelected: (value) async {
- switch (value) {
- case 'Pdf':
- FilePickerResult? result =
- await MediaService.pickPdfFile();
- if (result != null) {
- final File file =
- File(result.files.single.path!);
- state.file = file;
- // Do something with the selected PDF file
- }
- // else {
- //// User cancelled the file selection
- // }
- break;
-
- case 'Image':
- final pickedFile =
- await MediaService.pickImage(
- source: ImageSource.gallery);
- File? file;
- if (pickedFile != null && !kIsWeb) {
- file = await ImageCropper().cropImage(
- sourcePath: pickedFile.path,
- androidUiSettings:
- const AndroidUiSettings(
- toolbarTitle: 'برش تصویر'),
- iosUiSettings: const IOSUiSettings(
- title: 'برش تصویر',
- doneButtonTitle: 'تایید',
- cancelButtonTitle: 'بازگشت',
- ),
- compressQuality: 30,
- );
- if (file == null) return;
- }
- if (pickedFile == null) return;
- state.file =
- kIsWeb ? File(pickedFile.path) : file;
-
- break;
- default:
- }
-
- state.update();
- },
- itemBuilder: (BuildContext context) =>
- [
- popUpBtns(value: 'Pdf'),
- popUpBtns(value: 'Image'),
- ],
- offset: const Offset(0, -140),
- position: PopupMenuPosition.over,
- child: Icon(
- Icons.attach_file_rounded,
- color:
- Theme.of(context).colorScheme.focusedBorder,
- ),
- ),
- ),
- ),
- ],
+ DidvanIconButton(
+ icon: DidvanIcons.angle_right_solid,
+ onPressed: () {
+ Navigator.of(context).pop();
+ context.read().getChats();
+ },
),
],
- ));
- }),
- ),
- );
- }
-
- AnimatedVisibility fileContainer(BuildContext context) {
- final state = context.read();
- String basename = '';
- if (state.file != null) {
- basename = p.basename(state.file!.path);
- }
- return AnimatedVisibility(
- isVisible: state.file != null,
- duration: DesignConfig.lowAnimationDuration,
- child: Container(
- decoration: BoxDecoration(
- borderRadius: DesignConfig.mediumBorderRadius,
- color: Theme.of(context).colorScheme.border,
- ),
- padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
- margin: widget.args.bot.attachment! != 1
- ? const EdgeInsets.symmetric(horizontal: 12)
- : EdgeInsets.zero,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Row(
- children: [
- const Icon(Icons.file_copy),
- const SizedBox(
- width: 12,
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- SizedBox(
- width: 160,
- height: 24,
- child: MarqueeText(
- text: basename,
- style: const TextStyle(fontSize: 14),
- stop: const Duration(seconds: 3),
- ),
- ),
- if (state.file != null)
- FutureBuilder(
- future: state.file!.length(),
- builder: (context, snapshot) {
- if (!snapshot.hasData) {
- return const SizedBox();
- }
- return DidvanText(
- 'File Size ${(snapshot.data! / 1000).round()} KB',
- fontSize: 12,
- );
- })
- ],
- )
+ ),
+ actions: [
+ if (state.chatId != null)
+ Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: InkWell(
+ onTap: () {
+ final TextEditingController placeholder =
+ TextEditingController(
+ text: state.chat?.placeholder);
+ ActionSheetUtils.openDialog(
+ data: ActionSheetData(
+ hasConfirmButtonClose: false,
+ onConfirmed: () async {
+ final state = context.read();
+ await state
+ .changePlaceHolder(placeholder.text);
+ ActionSheetUtils.pop();
+ },
+ content: ValueListenableBuilder(
+ valueListenable: state.changingPlaceHolder,
+ builder: (context, value, child) => Column(
+ children: [
+ Stack(
+ children: [
+ Row(
+ mainAxisAlignment:
+ MainAxisAlignment.center,
+ children: [
+ DidvanText(
+ 'شخصیسازی دستورات',
+ style: Theme.of(context)
+ .textTheme
+ .titleMedium,
+ ),
+ ],
+ ),
+ Positioned(
+ right: 0,
+ top: 0,
+ bottom: 0,
+ child: Center(
+ child: InkWell(
+ onTap: () {
+ ActionSheetUtils.pop();
+ },
+ child: const Icon(
+ DidvanIcons.close_solid,
+ size: 24,
+ ),
+ ),
+ )),
+ ],
+ ),
+ const SizedBox(
+ height: 12,
+ ),
+ const DidvanText(
+ 'دوست دارید هوشان، چه چیزهایی را درباره شما بداند تا بتواند پاسخهای بهتری ارائه دهد؟ \nدستورات و اطلاعات ارائه شما، بر روی تمامی پیامهایی که از این به بعد ارسال میکنید، اعمال خواهد شد.'),
+ const SizedBox(
+ height: 12,
+ ),
+ value
+ ? Center(
+ child: Image.asset(
+ Assets.loadingAnimation,
+ width: 60,
+ height: 60,
+ ),
+ )
+ : TextField(
+ controller: placeholder,
+ style: (Theme.of(context)
+ .textTheme
+ .bodyMedium)!
+ .copyWith(
+ fontFamily: DesignConfig
+ .fontFamily
+ .padRight(3)),
+ decoration: InputDecoration(
+ filled: true,
+ fillColor: Theme.of(context)
+ .colorScheme
+ .secondCTA,
+ contentPadding:
+ const EdgeInsets.fromLTRB(
+ 10, 10, 10, 0),
+ border: const OutlineInputBorder(
+ borderRadius: DesignConfig
+ .lowBorderRadius),
+ errorStyle: const TextStyle(
+ height: 0.01),
+ ),
+ )
+ ],
+ ),
+ )));
+ },
+ child: const Icon(Icons.shopping_bag_outlined)),
+ )
],
+ centerTitle: true,
+ automaticallyImplyLeading: false,
),
- InkWell(
- onTap: () {
- state.file = null;
- state.update();
- },
- child: const Icon(DidvanIcons.close_circle_solid))
- ],
- ),
- ),
- );
- }
-
- PopupMenuItem popUpBtns({required final String value}) {
- return PopupMenuItem(
- value: value,
- height: 46,
- child: Row(
- children: [
- const Icon(Icons.picture_as_pdf_rounded),
- const SizedBox(
- width: 12,
- ),
- DidvanText(
- value,
- ),
- ],
+ body: state.loading
+ ? Center(
+ child: Image.asset(
+ Assets.loadingAnimation,
+ width: 60,
+ height: 60,
+ ),
+ )
+ : state.messages.isEmpty
+ ? Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const SizedBox(
+ height: 12,
+ ),
+ Center(
+ child: Icon(
+ DidvanIcons.ai_solid,
+ size: MediaQuery.sizeOf(context).width / 5,
+ ),
+ ),
+ const DidvanText('هوشان'),
+ const SizedBox(
+ height: 24,
+ ),
+ Container(
+ width: MediaQuery.sizeOf(context).height / 5,
+ height: MediaQuery.sizeOf(context).height / 5,
+ decoration: BoxDecoration(
+ borderRadius: DesignConfig.highBorderRadius,
+ color: Theme.of(context).colorScheme.focused),
+ padding: const EdgeInsets.all(12),
+ child: CachedNetworkImage(
+ imageUrl: widget.args.bot.image.toString(),
+ ),
+ ),
+ const SizedBox(
+ height: 24,
+ ),
+ Padding(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 20.0),
+ child: Text(
+ "به هوشان, هوش مصنوعی دیدوان خوش آمدید. \nبرای شروع گفتگو پیام مورد نظر خود را در کادر زیر بنویسید.\n دریافت پاسخ از: ${widget.args.bot.name}",
+ textAlign: TextAlign.center,
+ ),
+ )
+ ],
+ )
+ : SingleChildScrollView(
+ reverse: true,
+ controller: state.scrollController,
+ child: ListView.builder(
+ itemCount: state.messages.length,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ padding: const EdgeInsets.only(bottom: 90),
+ 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);
+ },
+ ),
+ ],
+ );
+ }),
+ ),
+ bottomSheet: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 8, bottom: 24.0, left: 12, right: 12),
+ child: AiMessageBar(
+ bot: widget.args.bot,
+ focusNode: focusNode,
+ ),
+ ),
+ ],
+ )),
),
);
}
@@ -455,6 +309,9 @@ class _AiChatPageState extends State {
Padding messageBubble(Prompts message, BuildContext context,
AiChatState state, int index, int mIndex) {
+ FilesModel? file =
+ message.file == null ? null : FilesModel(message.file.toString());
+
MarkdownStyleSheet defaultMarkdownStyleSheet = MarkdownStyleSheet(
code: TextStyle(
backgroundColor: Theme.of(context).colorScheme.black,
@@ -489,10 +346,12 @@ class _AiChatPageState extends State {
? Radius.zero
: null,
),
- color: (message.role.toString().contains('user')
- ? Theme.of(context).colorScheme.surface
- : Theme.of(context).colorScheme.focused)
- .withOpacity(0.9),
+ 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,
@@ -502,66 +361,131 @@ class _AiChatPageState extends State {
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width / 1.5),
child: message.finished != null && !message.finished!
- ? StreamBuilder(
- stream: state.messageOnstream,
- builder: (context, snapshot) {
- if (!snapshot.hasData) {
- return const SizedBox();
- }
- return Markdown(
- data: "${snapshot.data}...",
- selectable: false,
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- styleSheet: defaultMarkdownStyleSheet);
- },
+ ? Column(
+ children: [
+ ValueListenableBuilder>(
+ valueListenable: state.messageOnstream,
+ builder: (context, value, child) {
+ return StreamBuilder(
+ stream: value,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return const SizedBox();
+ }
+ return Markdown(
+ data: "${snapshot.data}...",
+ selectable: false,
+ shrinkWrap: true,
+ physics:
+ const NeverScrollableScrollPhysics(),
+ styleSheet:
+ defaultMarkdownStyleSheet);
+ },
+ );
+ }),
+ SpinKitThreeBounce(
+ color: Theme.of(context).colorScheme.primary,
+ size: 18,
+ )
+ ],
)
: Column(
children: [
if (message.role.toString().contains('user') &&
- message.file != null)
- Container(
- decoration: BoxDecoration(
- borderRadius: DesignConfig.mediumBorderRadius,
- color: Theme.of(context).colorScheme.border,
- ),
- padding:
- const EdgeInsets.fromLTRB(12, 8, 12, 8),
- margin: widget.args.bot.attachment! != 1
- ? const EdgeInsets.symmetric(horizontal: 12)
- : EdgeInsets.zero,
- child: Row(
- children: [
- const Icon(Icons.file_copy),
- const SizedBox(
- width: 12,
- ),
- Column(
- crossAxisAlignment:
- CrossAxisAlignment.start,
- children: [
- SizedBox(
- width: 200,
- child: DidvanText(
- (message.fileName.toString())),
- ),
- if (state.file != null)
- FutureBuilder(
- future: state.file!.length(),
- builder: (context, snapshot) {
- if (!snapshot.hasData) {
- return const SizedBox();
- }
- return DidvanText(
- 'File Size ${(snapshot.data! / 1000).round()} KB',
- fontSize: 12,
- );
- })
- ],
+ file != null)
+ lookupMimeType(file.path)?.startsWith('audio/') ??
+ false
+ ? Directionality(
+ textDirection: TextDirection.ltr,
+ child: FutureBuilder(
+ future: StorageService.getValue(
+ key: 'token'),
+ builder: (context, snapshot) {
+ return VoiceMessageView(
+ size: 32,
+ controller: VoiceController(
+ audioSrc: file.path
+ .startsWith('/uploads')
+ ? 'https://api.didvan.app${file.path}?accessToken=${snapshot.data}'
+ : file.path,
+ onComplete: () {
+ /// do something on complete
+ },
+ onPause: () {
+ /// do something on pause
+ },
+ onPlaying: () {
+ /// do something on playing
+ },
+ onError: (err) {
+ /// do somethin on error
+ },
+ isFile: !file.path
+ .startsWith('/uploads'),
+ maxDuration:
+ const Duration(seconds: 10),
+ ),
+ innerPadding: 0,
+ cornerRadius: 20,
+ circlesColor: Theme.of(context)
+ .colorScheme
+ .primary,
+ activeSliderColor:
+ Theme.of(context)
+ .colorScheme
+ .primary,
+ );
+ }),
)
- ],
- ),
- ),
+ : Container(
+ decoration: BoxDecoration(
+ borderRadius:
+ DesignConfig.mediumBorderRadius,
+ color: Theme.of(context)
+ .colorScheme
+ .border,
+ ),
+ constraints:
+ const BoxConstraints(minWidth: 200),
+ padding: const EdgeInsets.fromLTRB(
+ 12, 8, 12, 8),
+ margin: const EdgeInsets.symmetric(
+ horizontal: 12),
+ child: Row(
+ children: [
+ const Icon(Icons.file_copy),
+ const SizedBox(
+ width: 12,
+ ),
+ Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: 200,
+ child: DidvanText((message
+ .fileName
+ .toString())),
+ ),
+ if (state.file != null)
+ FutureBuilder(
+ future: state.file!.main
+ .length(),
+ builder:
+ (context, snapshot) {
+ if (!snapshot.hasData) {
+ return const SizedBox();
+ }
+ return DidvanText(
+ 'File Size ${(snapshot.data! / 1000).round()} KB',
+ fontSize: 12,
+ );
+ })
+ ],
+ )
+ ],
+ ),
+ ),
if (message.text != null)
Markdown(
data: message.text.toString(),
@@ -570,33 +494,75 @@ class _AiChatPageState extends State {
physics: const NeverScrollableScrollPhysics(),
styleSheet: defaultMarkdownStyleSheet,
),
- if (!message.role.toString().contains('user'))
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ if (message.error != null && message.error!)
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
- await Clipboard.setData(ClipboardData(
- text: state.messages[mIndex]
- .prompts[index].text
- .toString()));
- ActionSheetUtils.showAlert(AlertData(
- message: "متن با موفقیت کپی شد",
- aLertType: ALertType.success));
+ state.messages.last.prompts
+ .remove(message);
+ state.messages.last.prompts.add(
+ message.copyWith(error: false));
+ state.update();
+ await state
+ .postMessage(widget.args.bot);
},
child: Icon(
- DidvanIcons.copy_regular,
+ DidvanIcons.refresh_solid,
size: 18,
color: Theme.of(context)
.colorScheme
.focusedBorder,
),
),
- )
- ],
- )
+ ),
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: InkWell(
+ onTap: () async {
+ await Clipboard.setData(ClipboardData(
+ text: state.messages[mIndex]
+ .prompts[index].text
+ .toString()));
+ ActionSheetUtils.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);
+ }
+ },
+ child: Icon(
+ DidvanIcons.trash_solid,
+ size: 18,
+ color: Theme.of(context)
+ .colorScheme
+ .focusedBorder,
+ ),
+ ),
+ ),
+ ],
+ )
],
),
),
diff --git a/lib/views/ai/ai_chat_state.dart b/lib/views/ai/ai_chat_state.dart
index 73b2fb5..55d8048 100644
--- a/lib/views/ai/ai_chat_state.dart
+++ b/lib/views/ai/ai_chat_state.dart
@@ -1,28 +1,33 @@
import 'dart:convert';
-import 'dart:io';
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/enums.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/ai/ai_api_service.dart';
+import 'package:didvan/services/app_initalizer.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:flutter/cupertino.dart';
import 'package:flutter/material.dart';
+import 'package:get/get.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class AiChatState extends CoreProvier {
- Stream messageOnstream = const Stream.empty();
+ ValueNotifier> messageOnstream =
+ ValueNotifier(const Stream.empty());
List messages = [];
bool onResponsing = false;
bool loading = false;
+ ValueNotifier changingPlaceHolder = ValueNotifier(false);
final ScrollController scrollController = ScrollController();
int? chatId;
- File? file;
+ ChatsModel? chat;
+ FilesModel? file;
Future _scrolledEnd() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
@@ -38,7 +43,7 @@ class AiChatState extends CoreProvier {
onResponsing = false;
messages.last.prompts.removeLast();
messages.last.prompts.removeLast();
- messageOnstream = const Stream.empty();
+ messageOnstream.value = const Stream.empty();
await ActionSheetUtils.showAlert(AlertData(
message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
@@ -54,6 +59,7 @@ class AiChatState extends CoreProvier {
if (service.isSuccess) {
final id = service.result['id'];
chatId = id;
+ chat ??= ChatsModel(id: chatId);
}
return service;
}
@@ -61,6 +67,7 @@ class AiChatState extends CoreProvier {
Future getAllMessages(int chatId) async {
loading = true;
onResponsing = true;
+ update();
final service = RequestService(
RequestHelper.aiAChat(chatId),
);
@@ -99,6 +106,35 @@ class AiChatState extends CoreProvier {
update();
}
+ Future changePlaceHolder(String placeholder) async {
+ changingPlaceHolder.value = true;
+ update();
+ await Future.delayed(const Duration(seconds: 3));
+
+ final service = RequestService(RequestHelper.placeholder(chatId!),
+ body: {'placeholder': placeholder});
+ await service.put();
+ if (service.isSuccess) {
+ appState = AppState.idle;
+
+ // Add this code to scroll to maxScrollExtent after the ListView is built
+ if (chat == null) {
+ chat = ChatsModel(id: chatId, placeholder: placeholder);
+ } else {
+ chat = chat!.copyWith(placeholder: placeholder);
+ }
+ changingPlaceHolder.value = false;
+
+ update();
+
+ return;
+ }
+ appState = AppState.failed;
+ changingPlaceHolder.value = false;
+
+ update();
+ }
+
Future postMessage(BotsModel bot) async {
onResponsing = true;
@@ -107,6 +143,7 @@ class AiChatState extends CoreProvier {
messages.last.prompts.add(Prompts(
finished: false,
+ error: false,
text: '...',
role: 'bot',
createdAt: DateTime.now()
@@ -118,18 +155,27 @@ class AiChatState extends CoreProvier {
url: '/${bot.id}/${bot.name}'.toLowerCase(),
message: message,
chatId: chatId,
- file: file);
+ file: file?.main);
final res = await AiApiService.getResponse(req).catchError((e) {
_onError(e);
- throw e;
+ return e;
});
+
String responseMessgae = '';
+ String dataMessgae = '';
+ file = null;
+ update();
var r = res.listen((value) async {
var str = utf8.decode(value);
+ if (str.contains('{{{')) {
+ dataMessgae += str;
+ update();
+ return;
+ }
responseMessgae += str;
- messageOnstream = Stream.value(responseMessgae);
+ messageOnstream.value = Stream.value(responseMessgae);
- update();
+ // update();
});
r.onDone(() async {
@@ -141,11 +187,39 @@ class AiChatState extends CoreProvier {
}
}
onResponsing = false;
- messages.last.prompts.last = messages.last.prompts.last.copyWith(
- finished: true,
- text: responseMessgae,
- );
- messageOnstream = const Stream.empty();
+ if (responseMessgae.isEmpty) {
+ messages.last.prompts.removeLast();
+ messages.last.prompts.last =
+ messages.last.prompts.last.copyWith(error: true);
+ messageOnstream.value = const Stream.empty();
+ update();
+ _scrolledEnd();
+ return;
+ } else {
+ int? humanMessageId;
+ int? aiMessageId;
+ try {
+ final data = AppInitializer.messagesData(dataMessgae);
+ humanMessageId = data['HUMAN_MESSAGE_ID'];
+ aiMessageId = data['AI_MESSAGE_ID'];
+ } catch (e) {
+ e.printError();
+
+ return;
+ }
+ // Access the values
+ messages.last.prompts.last = messages.last.prompts.last
+ .copyWith(finished: true, text: responseMessgae, id: aiMessageId);
+ if (messages.last.prompts.length > 2) {
+ messages.last.prompts[messages.last.prompts.length - 2] = messages
+ .last.prompts[messages.last.prompts.length - 2]
+ .copyWith(id: humanMessageId);
+ } else {
+ messages.last.prompts.first =
+ messages.last.prompts.first.copyWith(id: humanMessageId);
+ }
+ messageOnstream.value = const Stream.empty();
+ }
update();
_scrolledEnd();
@@ -153,4 +227,27 @@ class AiChatState extends CoreProvier {
r.onError(_onError);
}
+
+ Future deleteMessage(int id, int mIndex, int index) async {
+ final service = RequestService(RequestHelper.deleteMessage(chatId!, id));
+ await service.delete();
+ if (service.isSuccess) {
+ if (messages[mIndex].prompts.length <= 1) {
+ messages.removeAt(mIndex);
+ } else {
+ messages[mIndex].prompts.removeAt(index);
+ }
+
+ appState = AppState.idle;
+
+ update();
+
+ return;
+ }
+ appState = AppState.failed;
+ await ActionSheetUtils.showAlert(AlertData(
+ message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
+
+ update();
+ }
}
diff --git a/lib/views/ai/history_ai_chat_page.dart b/lib/views/ai/history_ai_chat_page.dart
index 9081880..4e94225 100644
--- a/lib/views/ai/history_ai_chat_page.dart
+++ b/lib/views/ai/history_ai_chat_page.dart
@@ -1,4 +1,4 @@
-// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: library_private_types_in_public_api, deprecated_member_use
import 'dart:async';
@@ -9,12 +9,14 @@ import 'package:didvan/constants/assets.dart';
import 'package:didvan/main.dart';
import 'package:didvan/models/ai/ai_chat_args.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
+import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
+import 'package:didvan/views/widgets/search_field.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart';
@@ -22,7 +24,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class HistoryAiChatPage extends StatefulWidget {
- const HistoryAiChatPage({Key? key}) : super(key: key);
+ final bool? archived;
+ const HistoryAiChatPage({Key? key, required this.archived}) : super(key: key);
@override
_HistoryAiChatPageState createState() => _HistoryAiChatPageState();
@@ -35,73 +38,184 @@ class _HistoryAiChatPageState extends State {
final state = context.read();
Future.delayed(
Duration.zero,
- () => state.getChats(),
+ () => state.getChats(archived: widget.archived),
);
super.initState();
}
+ Timer? _timer;
+ late bool archived = widget.archived ?? false;
+
@override
Widget build(BuildContext context) {
- return DidvanScaffold(
- hidePlayer: true,
- physics: const BouncingScrollPhysics(),
- padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92),
- floatingActionButton: openAiListBtn(context),
- scrollController: scrollController,
- slivers: [
- Consumer(
- builder: (context, state, child) {
- return SliverStateHandler(
- state: state,
- emptyState: EmptyState(
- asset: Assets.emptyResult,
- title: 'لیست خالی است',
- ),
- enableEmptyState: state.chats.isEmpty,
- placeholder: const _HistoryPlaceholder(),
- placeholderCount: 8,
+ return WillPopScope(
+ onWillPop: () async {
+ await context.read().getChats();
+ return true;
+ },
+ child: DidvanScaffold(
+ hidePlayer: true,
+ physics: const BouncingScrollPhysics(),
+ // floatingActionButton: openAiListBtn(context),
+ padding: EdgeInsets.zero,
+ scrollController: scrollController,
+ showSliversFirst: false,
+ slivers: [
+ Consumer(
+ builder: (context, state, child) {
+ return SliverStateHandler(
+ state: state,
+ emptyState: EmptyState(
+ asset: Assets.emptyResult,
+ title: 'لیست خالی است',
+ ),
+ enableEmptyState: state.chats.isEmpty,
+ placeholder: const _HistoryPlaceholder(),
+ placeholderCount: 8,
- // builder: (context, state, index) => _HistoryPlaceholder(),
- builder: (context, state, index) {
- final chat = state.chats[index];
+ // builder: (context, state, index) => _HistoryPlaceholder(),
+ builder: (context, state, index) {
+ final chat = state.chats[index];
+ TextEditingController title =
+ TextEditingController(text: state.chats[index].title);
+ return Dismissible(
+ key: UniqueKey(),
+ background: Container(
+ color: Theme.of(context).colorScheme.error,
+ alignment: Alignment.centerRight,
+ padding: const EdgeInsets.symmetric(horizontal: 20.0),
+ child: Icon(
+ DidvanIcons.trash_solid,
+ color: Theme.of(context).colorScheme.white,
+ ),
+ ),
+ secondaryBackground: Container(
+ color: Theme.of(context).colorScheme.primary,
+ alignment: Alignment.centerLeft,
+ padding: const EdgeInsets.symmetric(horizontal: 20.0),
+ child: Icon(
+ archived
+ ? Icons.folder_delete
+ : Icons.create_new_folder_rounded,
+ color: Theme.of(context).colorScheme.white),
+ ),
+ movementDuration: const Duration(milliseconds: 600),
+ confirmDismiss: (direction) async {
+ bool result = false;
- return InkWell(
- onTap: () {
- if (state.chatsToDelete.isEmpty) {
- navigatorKey.currentState!.pushNamed(Routes.aiChat,
- arguments:
- AiChatArgs(bot: chat.bot!, chatId: chat.id!));
- } else {
- if (state.chatsToDelete.contains(chat.id)) {
- state.chatsToDelete.remove(chat.id!);
+ if (direction == DismissDirection.startToEnd) {
+ ActionSheetUtils.context = context;
+ await ActionSheetUtils.openDialog(
+ data: ActionSheetData(
+ onConfirmed: () async {
+ final state =
+ context.read();
+ await state.deleteChat(chat.id!, index);
+ result = true;
+ },
+ content: Column(
+ children: [
+ Row(
+ crossAxisAlignment:
+ CrossAxisAlignment.center,
+ children: [
+ Icon(
+ DidvanIcons.trash_solid,
+ color: Theme.of(context)
+ .colorScheme
+ .error,
+ ),
+ const SizedBox(
+ width: 8,
+ ),
+ SizedBox(
+ child: DidvanText(
+ 'پاک کردن گفتوگو',
+ color: Theme.of(context)
+ .colorScheme
+ .error,
+ fontSize: 20,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 12,
+ ),
+ SizedBox(
+ child: RichText(
+ text: TextSpan(
+ text:
+ 'آیا از پاک کردن گفتوگوی ',
+ style: TextStyle(
+ color: Theme.of(context)
+ .colorScheme
+ .text),
+ children: [
+ TextSpan(
+ text: "\"${chat.title}\"",
+ style: const TextStyle(
+ fontWeight:
+ FontWeight.bold)),
+ const TextSpan(
+ text:
+ ' با هوشان اطمینان دارید؟ '),
+ ]),
+ ),
+ ),
+ ],
+ )));
} else {
- state.chatsToDelete.add(chat.id!);
+ result = await state.archivedChat(chat.id!, index);
}
- }
- state.update();
- },
- onLongPress: () {
- if (state.chatsToDelete.isEmpty) {
- state.chatsToDelete.add(chat.id!);
- }
- state.update();
- },
- child: Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color:
- Theme.of(context).colorScheme.border))),
- child: Stack(
- children: [
- Row(
+ return result;
+ },
+ child: InkWell(
+ onTap: () {
+ // if (state.chatsToDelete.isEmpty) {
+ navigatorKey.currentState!.pushNamed(Routes.aiChat,
+ arguments:
+ AiChatArgs(bot: chat.bot!, chat: chat));
+ // } else {
+ // if (state.chatsToDelete.contains(chat.id)) {
+ // state.chatsToDelete.remove(chat.id!);
+ // } else {
+ // state.chatsToDelete.add(chat.id!);
+ // }
+ // }
+ // state.update();
+ },
+ onLongPress: () {
+ state.chats[index] =
+ state.chats[index].copyWith(isEditing: true);
+ state.update();
+
+ // if (state.chatsToDelete.isEmpty) {
+ // state.chatsToDelete.add(chat.id!);
+ // }
+ // state.update();
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12, horizontal: 20),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: Theme.of(context)
+ .colorScheme
+ .border))),
+ child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
- const SizedBox(
+ SizedBox(
width: 46,
height: 46,
+ child: ClipOval(
+ child: CachedNetworkImage(
+ imageUrl: chat.bot!.image.toString(),
+ ),
+ ),
),
const SizedBox(
width: 18,
@@ -117,6 +231,7 @@ class _HistoryAiChatPageState extends State {
DidvanText(
chat.bot!.name.toString(),
fontWeight: FontWeight.bold,
+ // fontSize: 18,
),
DidvanText(
DateTimeUtils.momentGenerator(
@@ -126,10 +241,103 @@ class _HistoryAiChatPageState extends State {
],
),
SizedBox(
- child: DidvanText(
- chat.prompts![0].text.toString(),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ chat.isEditing != null &&
+ chat.isEditing!
+ ? Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ controller: title,
+ style:
+ const TextStyle(
+ fontSize: 12),
+ textAlignVertical:
+ TextAlignVertical
+ .bottom,
+ maxLines: 1,
+ decoration:
+ const InputDecoration(
+ isDense: true,
+ contentPadding:
+ EdgeInsets
+ .symmetric(
+ vertical:
+ 5,
+ horizontal:
+ 10),
+ border:
+ OutlineInputBorder(),
+ )),
+ ),
+ const SizedBox(
+ width: 12,
+ ),
+ state.loadingchangeTitle
+ ? const SizedBox(
+ width: 12,
+ height: 12,
+ child:
+ CircularProgressIndicator())
+ : InkWell(
+ onTap: () async {
+ if (title.text
+ .toString() ==
+ chat.title
+ .toString()) {
+ chat.isEditing =
+ false;
+ state.update();
+ return;
+ }
+ if (title.text
+ .isNotEmpty) {
+ await state
+ .changeNameChat(
+ chat
+ .id!,
+ index,
+ title
+ .text);
+ title.clear();
+ }
+ if (chat.isEditing !=
+ null) {
+ chat.isEditing =
+ !chat
+ .isEditing!;
+ state.update();
+ return;
+ }
+ chat.isEditing =
+ true;
+
+ state.update();
+ },
+ child: const Icon(
+ DidvanIcons
+ .check_circle_solid),
+ )
+ ],
+ )
+ : DidvanText(
+ chat.title.toString(),
+ maxLines: 1,
+ overflow:
+ TextOverflow.ellipsis,
+ // fontWeight: FontWeight.bold,
+ // fontSize: 16,
+ ),
+ DidvanText(
+ chat.prompts![0].text.toString(),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ fontSize: 12,
+ ),
+ ],
),
),
],
@@ -137,184 +345,111 @@ class _HistoryAiChatPageState extends State {
),
],
),
- Positioned(
- bottom: 0,
- right: 0,
- top: 0,
- child: Padding(
- padding: const EdgeInsets.all(4.0),
- child: ClipOval(
- child: CachedNetworkImage(
- imageUrl: chat.bot!.image.toString(),
- ),
- ),
- ),
- ),
- if (state.chatsToDelete.contains(chat.id))
- Positioned(
- right: 32,
- bottom: 0,
- child: Container(
- // ignore: prefer_const_constructors
- decoration: BoxDecoration(
- color: Theme.of(context)
- .scaffoldBackgroundColor,
- shape: BoxShape.circle),
- child: Icon(DidvanIcons.check_circle_solid,
- size: 20,
- color:
- Theme.of(context).colorScheme.success),
- ),
- )
- ],
+ ),
),
- ),
- );
+ );
+ },
+ childCount: state.chats.length,
+ onRetry: state.getChats);
+ },
+ )
+ ],
+ appBarData: AppBarData(
+ title: archived ? 'گفتوگوهای آرشیو شده' : 'تاریخچه گفتوگوها',
+ hasBack: true,
+ hasElevation: true,
+ trailing: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20.0),
+ child: InkWell(
+ onTap: () async {
+ ActionSheetUtils.context = context;
+ await ActionSheetUtils.openDialog(
+ data: ActionSheetData(
+ onConfirmed: () async {
+ final state = context.read();
+ await state.deleteAllChat();
+ },
+ content: Column(
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(
+ DidvanIcons.trash_solid,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ const SizedBox(
+ width: 8,
+ ),
+ DidvanText(
+ 'پاک کردن همه گفتوگوها',
+ color: Theme.of(context).colorScheme.error,
+ fontSize: 20,
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 12,
+ ),
+ const DidvanText(
+ 'آیا از پاک کردن تمامی گفتوگوهای انجام شده با هوشان اطمینان دارید؟'),
+ ],
+ )));
},
- childCount: state.chats.length,
- onRetry: state.getChats);
- },
- )
- ],
- appBarData: null,
+ child: DidvanText(
+ archived ? 'خارج کردن همه' : 'حذف همه',
+ color: Theme.of(context).colorScheme.error,
+ ),
+ ),
+ )),
+ children: [
+ Container(
+ color: Theme.of(context).colorScheme.surface,
+ padding: const EdgeInsets.only(right: 20, left: 20, bottom: 24),
+ child: SearchField(
+ title: 'گفتوگوها',
+ onChanged: (value) {
+ final state = context.read();
+ if (value.isEmpty) {
+ state.getChats(archived: widget.archived);
+ return;
+ }
+ _timer?.cancel();
+ _timer = Timer(const Duration(seconds: 1), () {
+ state.search = value;
+ state.getSearchChats(q: value, archived: widget.archived);
+ });
+ },
+ focusNode: FocusNode()),
+ ),
+ ],
+ ),
);
}
- Widget openAiListBtn(BuildContext context) {
- final watch = context.watch();
- final state = context.read();
- return FloatingActionButton(
- backgroundColor: watch.chatsToDelete.isEmpty
- ? Theme.of(context).colorScheme.primary
- : Theme.of(context).colorScheme.error,
- shape: const OvalBorder(),
- mini: true,
- onPressed: () {
- if (watch.chatsToDelete.isEmpty) {
- state.getBots();
- state.search = '';
- _botsDialogSelect(context);
- } else {
- state.addChatToDelete();
- }
- },
- child: watch.chatsToDelete.isEmpty
- ? const Icon(DidvanIcons.add_regular)
- : const Icon(DidvanIcons.trash_regular),
- );
- }
-
- void _botsDialogSelect(BuildContext context) {
- final state = context.read();
-
- ActionSheetUtils.context = context;
- ActionSheetUtils.openDialog(
- data: ActionSheetData(
- hasConfirmButton: false,
- hasDismissButton: false,
- content: Column(
- children: [
- // Row(
- // mainAxisAlignment: MainAxisAlignment.end,
- // children: [
- // Padding(
- // padding: const EdgeInsets.symmetric(vertical: 8.0),
- // child: InkWell(
- // onTap: () {
- // ActionSheetUtils.pop();
- // },
- // child: const Icon(DidvanIcons.close_solid)),
- // )
- // ],
- // ),
- // SearchField(
- // title: 'هوش مصنوعی',
- // value: state.search,
- // onChanged: (value) {
- // state.search = value;
- // if (value.isEmpty) {
- // state.getBots();
- // return;
- // }
- // state.timer?.cancel();
- // state.timer = Timer(const Duration(seconds: 1), () {
- // state.getSearchBots(value);
- // });
- // },
- // focusNode: FocusNode()),
- // const SizedBox(
- // height: 12,
- // ),
- SizedBox(
- width: double.infinity,
- height: MediaQuery.sizeOf(context).height / 3,
- child: ValueListenableBuilder(
- valueListenable: state.loadingBots,
- builder: (context, value, child) => value
- ? Center(
- child: Image.asset(
- Assets.loadingAnimation,
- width: 60,
- height: 60,
- ),
- )
- : state.bots.isEmpty
- ? Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 12.0),
- child: EmptyState(
- asset: Assets.emptyResult,
- title: 'نتیجهای پیدا نشد',
- height: 120,
- ),
- )
- : ListView.builder(
- padding:
- const EdgeInsets.symmetric(vertical: 12),
- itemCount: state.bots.length,
- physics: const BouncingScrollPhysics(),
- shrinkWrap: true,
- itemBuilder: (context, index) {
- final bot = state.bots[index];
- return InkWell(
- onTap: () {
- ActionSheetUtils.pop();
- navigatorKey.currentState!.pushNamed(
- Routes.aiChat,
- arguments: AiChatArgs(bot: bot));
- },
- child: Container(
- alignment: Alignment.center,
- padding: const EdgeInsets.symmetric(
- vertical: 8),
- decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: Theme.of(context)
- .colorScheme
- .border,
- width: 1))),
- child: Row(
- children: [
- ClipOval(
- child: CachedNetworkImage(
- imageUrl: bot.image.toString(),
- width: 42,
- height: 42,
- ),
- ),
- const SizedBox(width: 12),
- Text(bot.name.toString())
- ],
- ),
- ),
- );
- }),
- ),
- )
- ],
- )));
- }
+ // Widget openAiListBtn(BuildContext context) {
+ // final watch = context.watch();
+ // final state = context.read();
+ // return FloatingActionButton(
+ // backgroundColor: watch.chatsToDelete.isEmpty
+ // ? Theme.of(context).colorScheme.primary
+ // : Theme.of(context).colorScheme.error,
+ // shape: const OvalBorder(),
+ // mini: true,
+ // onPressed: () {
+ // if (watch.chatsToDelete.isEmpty) {
+ // state.getBots();
+ // state.search = '';
+ // _botsDialogSelect(context);
+ // } else {
+ // state.addChatToDelete();
+ // }
+ // },
+ // child: watch.chatsToDelete.isEmpty
+ // ? const Icon(DidvanIcons.add_regular)
+ // : const Icon(DidvanIcons.trash_regular),
+ // );
+ // }
}
class _HistoryPlaceholder extends StatelessWidget {
diff --git a/lib/views/ai/history_ai_chat_state.dart b/lib/views/ai/history_ai_chat_state.dart
index 6da7030..31ba4bc 100644
--- a/lib/views/ai/history_ai_chat_state.dart
+++ b/lib/views/ai/history_ai_chat_state.dart
@@ -12,15 +12,20 @@ import 'package:flutter/cupertino.dart';
class HistoryAiChatState extends CoreProvier {
final List chats = [];
- final List chatsToDelete = [];
+ // final List chatsToDelete = [];
final List bots = [];
+ BotsModel? bot;
ValueNotifier loadingBots = ValueNotifier(false);
+ bool loadingchangeTitle = false;
+ bool loadingdeleteAll = false;
Timer? timer;
String search = '';
- Future getChats() async {
+ Future getChats({final bool? archived}) async {
final service = RequestService(
- RequestHelper.aiChats(),
+ archived != null && archived
+ ? RequestHelper.aiArchived()
+ : RequestHelper.aiChats(),
);
await service.httpGet();
if (service.isSuccess) {
@@ -37,6 +42,29 @@ class HistoryAiChatState extends CoreProvier {
update();
}
+ Future getSearchChats(
+ {required final String q, final bool? archived}) async {
+ final service = RequestService(
+ archived != null && archived
+ ? RequestHelper.aiSearchArchived(q)
+ : RequestHelper.aiSearchChats(q),
+ );
+ await service.httpGet();
+ if (service.isSuccess) {
+ chats.clear();
+ final ch = service.result['chats'];
+ for (var i = 0; i < ch.length; i++) {
+ chats.add(ChatsModel.fromJson(ch[i]));
+ }
+ appState = AppState.idle;
+ update();
+
+ return;
+ }
+ appState = AppState.failed;
+ update();
+ }
+
Future getBots() async {
loadingBots.value = true;
final service = RequestService(
@@ -51,6 +79,7 @@ class HistoryAiChatState extends CoreProvier {
}
appState = AppState.idle;
loadingBots.value = false;
+ bot = bots.first;
update();
return;
}
@@ -82,22 +111,44 @@ class HistoryAiChatState extends CoreProvier {
update();
}
- Future addChatToDelete() async {
- final service = RequestService(RequestHelper.aiDeleteChats(),
- body: {"ids": chatsToDelete});
- await service.delete();
+ // Future addChatToDelete() async {
+ // final service = RequestService(RequestHelper.aiDeleteChats(),
+ // body: {"ids": chatsToDelete});
+ // await service.delete();
+ // if (service.isSuccess) {
+ // final List cs = [];
+ // for (var chat in chats) {
+ // if (!chatsToDelete.contains(chat.id)) {
+ // cs.add(chat);
+ // }
+ // }
+ // chatsToDelete.clear();
+ // chats.clear();
+ // chats.addAll(cs);
+
+ // appState = AppState.idle;
+ // update();
+
+ // return;
+ // }
+ // appState = AppState.failed;
+ // await ActionSheetUtils.showAlert(AlertData(
+ // message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
+
+ // update();
+ // }
+
+ Future changeNameChat(int id, int index, String title) async {
+ loadingchangeTitle = true;
+ update();
+ final service =
+ RequestService(RequestHelper.aiChangeChats(id), body: {"title": title});
+ await service.put();
if (service.isSuccess) {
- final List cs = [];
- for (var chat in chats) {
- if (!chatsToDelete.contains(chat.id)) {
- cs.add(chat);
- }
- }
- chatsToDelete.clear();
- chats.clear();
- chats.addAll(cs);
+ chats[index].title = title;
appState = AppState.idle;
+ loadingchangeTitle = false;
update();
return;
@@ -105,7 +156,77 @@ class HistoryAiChatState extends CoreProvier {
appState = AppState.failed;
await ActionSheetUtils.showAlert(AlertData(
message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
+ loadingchangeTitle = false;
update();
}
+
+ Future deleteChat(int id, int index) async {
+ final service = RequestService(RequestHelper.deleteChat(id));
+ await service.delete();
+ if (service.isSuccess) {
+ chats.removeAt(index);
+
+ appState = AppState.idle;
+ loadingchangeTitle = false;
+
+ update();
+
+ return;
+ }
+ appState = AppState.failed;
+ await ActionSheetUtils.showAlert(AlertData(
+ message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
+ loadingchangeTitle = false;
+
+ update();
+ }
+
+ Future deleteAllChat() async {
+ loadingdeleteAll = true;
+ update();
+ final service = RequestService(
+ RequestHelper.deleteAllChats(),
+ );
+ await service.delete();
+ if (service.isSuccess) {
+ chats.clear();
+
+ appState = AppState.idle;
+ loadingdeleteAll = false;
+
+ update();
+
+ return;
+ }
+ appState = AppState.failed;
+ await ActionSheetUtils.showAlert(AlertData(
+ message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
+ loadingdeleteAll = false;
+
+ update();
+ }
+
+ Future archivedChat(int id, int index) async {
+ update();
+ final service = RequestService(
+ RequestHelper.archivedChat(id),
+ );
+ await service.put();
+ if (service.isSuccess) {
+ chats.removeAt(index);
+
+ appState = AppState.idle;
+
+ update();
+
+ return true;
+ }
+ appState = AppState.failed;
+ await ActionSheetUtils.showAlert(AlertData(
+ message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
+
+ update();
+ return false;
+ }
}
diff --git a/lib/views/ai/widgets/ai_message_bar.dart b/lib/views/ai/widgets/ai_message_bar.dart
new file mode 100644
index 0000000..cf1c9b2
--- /dev/null
+++ b/lib/views/ai/widgets/ai_message_bar.dart
@@ -0,0 +1,633 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:didvan/config/design_config.dart';
+import 'package:didvan/config/theme_data.dart';
+import 'package:didvan/constants/app_icons.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/services/media/media.dart';
+import 'package:didvan/services/storage/storage.dart';
+import 'package:didvan/utils/date_time.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/message_bar_btn.dart';
+import 'package:didvan/views/ai/widgets/voice_message_view.dart';
+import 'package:didvan/views/widgets/animated_visibility.dart';
+import 'package:didvan/views/widgets/didvan/text.dart';
+import 'package:didvan/views/widgets/marquee_text.dart';
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_spinkit/flutter_spinkit.dart';
+import 'package:get/get.dart';
+import 'package:image_cropper/image_cropper.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:mime/mime.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:persian_number_utility/persian_number_utility.dart';
+import 'package:provider/provider.dart';
+import 'package:record/record.dart';
+
+import 'package:path/path.dart' as p;
+import 'package:voice_message_package/voice_message_package.dart';
+
+class AiMessageBar extends StatefulWidget {
+ final FocusNode? focusNode;
+ final BotsModel bot;
+ const AiMessageBar({
+ super.key,
+ this.focusNode,
+ required this.bot,
+ });
+
+ @override
+ State createState() => _AiMessageBarState();
+
+ static PopupMenuItem popUpBtns({
+ required final String value,
+ required final IconData icon,
+ final Color? color,
+ final double? height,
+ final double? size,
+ }) {
+ return PopupMenuItem(
+ value: value,
+ height: height ?? 46,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ icon,
+ color: color,
+ size: size,
+ ),
+ const SizedBox(
+ width: 12,
+ ),
+ DidvanText(
+ value,
+ color: color,
+ fontSize: size,
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _AiMessageBarState extends State {
+ TextEditingController message = TextEditingController();
+
+ @override
+ void initState() {
+ widget.focusNode?.addListener(() {});
+
+ super.initState();
+ }
+
+ final record = AudioRecorder();
+
+ @override
+ void dispose() {
+ super.dispose();
+ record.dispose();
+ try {
+ _timer.cancel();
+ } catch (e) {
+ e.printError();
+ }
+ }
+
+ late Timer _timer;
+ final ValueNotifier _countTimer = ValueNotifier(0);
+ void startTimer() {
+ const oneSec = Duration(seconds: 1);
+ _timer = Timer.periodic(
+ oneSec,
+ (Timer timer) {
+ _countTimer.value++;
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Consumer(
+ builder: (context, state, child) {
+ final historyState = context.read();
+
+ return IgnorePointer(
+ ignoring: state.onResponsing,
+ child: Column(
+ children: [
+ fileContainer(),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Expanded(
+ child: Column(
+ children: [
+ audioContainer(),
+ Container(
+ decoration: BoxDecoration(
+ boxShadow: DesignConfig.defaultShadow,
+ color: Theme.of(context).colorScheme.white,
+ borderRadius: BorderRadius.circular(360)),
+ child: Row(
+ children: [
+ const SizedBox(
+ width: 8,
+ ),
+ Expanded(
+ child: StreamBuilder(
+ stream: record.onStateChanged(),
+ builder: (context, snapshot) {
+ return Row(
+ children: [
+ (snapshot.hasData &&
+ snapshot.data! !=
+ RecordState.stop)
+ ? MessageBarBtn(
+ enable: true,
+ icon: DidvanIcons
+ .stop_circle_solid,
+ click: () async {
+ final path =
+ await record.stop();
+ state.file = FilesModel(
+ path.toString());
+ _timer.cancel();
+ _countTimer.value = 0;
+ state.update();
+ },
+ )
+ : widget.bot.attachmentType!
+ .contains('audio') &&
+ message.text.isEmpty &&
+ state.file == null &&
+ widget.bot.attachment != 0
+ ? MessageBarBtn(
+ enable: false,
+ icon:
+ DidvanIcons.mic_regular,
+ click: () async {
+ if (await record
+ .hasPermission()) {
+ Directory? downloadDir =
+ await getDownloadsDirectory();
+
+ record.start(
+ const RecordConfig(),
+ path:
+ '${downloadDir!.path}/${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a');
+ startTimer();
+ }
+ },
+ )
+ : MessageBarBtn(
+ enable: message
+ .text.isNotEmpty ||
+ state.file != null,
+ icon:
+ DidvanIcons.send_light,
+ click: () async {
+ if (state.file == null &&
+ message
+ .text.isEmpty) {
+ return;
+ }
+
+ 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: message.text,
+ file:
+ state.file?.path,
+ fileName: state
+ .file ==
+ null
+ ? null
+ : p.basename(state
+ .file!.path),
+ 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: message
+ .text,
+ finished: true,
+ file: state
+ .file?.path,
+ fileName: state
+ .file ==
+ null
+ ? null
+ : p.basename(state
+ .file!
+ .path),
+ role: 'user',
+ createdAt: DateTime
+ .now()
+ .subtract(const Duration(
+ minutes:
+ 210))
+ .toIso8601String(),
+ )
+ ]));
+ }
+ message.clear();
+ await state.postMessage(
+ widget.bot);
+ },
+ ),
+ const SizedBox(
+ width: 12,
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ ),
+ child: snapshot.hasData &&
+ (snapshot.data! ==
+ RecordState
+ .record ||
+ snapshot.data! ==
+ RecordState.pause)
+ ? Padding(
+ padding:
+ const EdgeInsets.all(
+ 8.0),
+ child: Row(
+ mainAxisAlignment:
+ MainAxisAlignment
+ .center,
+ children: [
+ SpinKitWave(
+ color:
+ Theme.of(context)
+ .colorScheme
+ .primary,
+ size: 32,
+ ),
+ const SizedBox(
+ width: 24,
+ ),
+ ValueListenableBuilder<
+ int>(
+ valueListenable:
+ _countTimer,
+ builder: (context,
+ value,
+ child) =>
+ DidvanText(DateTimeUtils
+ .normalizeTimeDuration(
+ Duration(
+ seconds:
+ value))),
+ )
+ ],
+ ),
+ )
+ : Form(
+ child: TextFormField(
+ textInputAction:
+ TextInputAction.newline,
+ style: Theme.of(context)
+ .textTheme
+ .bodyMedium,
+ maxLines: 2,
+ minLines: 1,
+ // keyboardType: TextInputType.text,
+
+ controller: message,
+ focusNode: widget.focusNode,
+ enabled: !(state.file !=
+ null &&
+ widget.bot.attachment ==
+ 1),
+
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ hintText: 'بنویسید...',
+ hintStyle: Theme.of(
+ context)
+ .textTheme
+ .bodySmall!
+ .copyWith(
+ color: Theme.of(
+ context)
+ .colorScheme
+ .disabledText),
+ ),
+ onChanged: (value) {
+ setState(() {});
+ },
+ )),
+ ),
+ ),
+ if (snapshot.hasData)
+ snapshot.data! == RecordState.record
+ ? MessageBarBtn(
+ enable: false,
+ icon: DidvanIcons.pause_solid,
+ click: () async {
+ await record.pause();
+ _timer.cancel();
+ },
+ )
+ : snapshot.data! ==
+ RecordState.pause
+ ? MessageBarBtn(
+ enable: false,
+ icon: DidvanIcons
+ .play_solid,
+ click: () async {
+ await record.resume();
+ startTimer();
+ },
+ )
+ : const SizedBox(),
+ const SizedBox(
+ width: 8,
+ ),
+ ],
+ );
+ }),
+ ),
+ const SizedBox(
+ width: 8,
+ ),
+ ],
+ ),
+ ),
+ ],
+ )),
+ const SizedBox(
+ width: 12,
+ ),
+ if (context
+ .read()
+ .bot!
+ .attachmentType!
+ .isNotEmpty &&
+ (widget.bot.attachment != 0 &&
+ (widget.bot.attachment == 1 &&
+ message.text.isEmpty)) ||
+ widget.bot.attachment == 2)
+ SizedBox(
+ width: 46,
+ height: 46,
+ child: Center(
+ child: PopupMenuButton(
+ onOpened: () {
+ },
+ onSelected: (value) async {
+ switch (value) {
+ case 'Pdf':
+ FilePickerResult? result =
+ await MediaService.pickPdfFile();
+ if (result != null) {
+ state.file =
+ FilesModel(result.files.single.path!);
+ // Do something with the selected PDF file
+ }
+ // else {
+ //// User cancelled the file selection
+ // }
+ break;
+
+ case 'Image':
+ final pickedFile =
+ await MediaService.pickImage(
+ source: ImageSource.gallery);
+ File? file;
+ if (pickedFile != null && !kIsWeb) {
+ file = await ImageCropper().cropImage(
+ sourcePath: pickedFile.path,
+ androidUiSettings:
+ const AndroidUiSettings(
+ toolbarTitle: 'برش تصویر'),
+ iosUiSettings: const IOSUiSettings(
+ title: 'برش تصویر',
+ doneButtonTitle: 'تایید',
+ cancelButtonTitle: 'بازگشت',
+ ),
+ compressQuality: 30,
+ );
+ if (file == null) return;
+ }
+ if (pickedFile == null) return;
+ state.file = kIsWeb
+ ? FilesModel(pickedFile.path)
+ : FilesModel(file!.path);
+
+ break;
+
+ case 'Audio':
+ FilePickerResult? result =
+ await MediaService.pickAudioFile();
+ if (result != null) {
+ state.file =
+ FilesModel(result.files.single.path!);
+ }
+ break;
+ default:
+ }
+
+ state.update();
+ },
+ itemBuilder: (BuildContext context) => [
+ if (historyState.bot!.attachmentType!
+ .contains('pdf'))
+ AiMessageBar.popUpBtns(
+ value: 'Pdf', icon: Icons.picture_as_pdf),
+ if (historyState.bot!.attachmentType!
+ .contains('image'))
+ AiMessageBar.popUpBtns(
+ value: 'Image', icon: Icons.image),
+ if (historyState.bot!.attachmentType!
+ .contains('audio'))
+ AiMessageBar.popUpBtns(
+ value: 'Audio', icon: Icons.audio_file),
+ ],
+ offset: Offset(
+ 0, widget.focusNode!.hasFocus ? -999 : 999),
+ position: PopupMenuPosition.over,
+ useRootNavigator: true,
+ child: Icon(
+ Icons.attach_file_rounded,
+ color:
+ Theme.of(context).colorScheme.focusedBorder,
+ ),
+ ),
+ )),
+ ],
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ AnimatedVisibility audioContainer() {
+ final state = context.watch();
+
+ return AnimatedVisibility(
+ isVisible: state.file != null &&
+ lookupMimeType(state.file!.path)!.startsWith('audio/'),
+ duration: DesignConfig.lowAnimationDuration,
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: FutureBuilder(
+ future: StorageService.getValue(key: 'token'),
+ builder: (context, snapshot) => SizedBox(
+ width: MediaQuery.sizeOf(context).width,
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(0, 0, 0, 8),
+ child: Container(
+ height: 46,
+ decoration: BoxDecoration(
+ boxShadow: DesignConfig.defaultShadow,
+ color: Theme.of(context).colorScheme.white,
+ borderRadius: BorderRadius.circular(360)),
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: state.file == null
+ ? const SizedBox()
+ : MyVoiceMessageView(
+ size: 32,
+ controller: VoiceController(
+ audioSrc: state.file!.path.startsWith('/uploads')
+ ? 'https://api.didvan.app${state.file!.path}?accessToken=${snapshot.data}'
+ : state.file!.path,
+ onComplete: () {
+ /// do something on complete
+ },
+ onPause: () {
+ /// do something on pause
+ },
+ onPlaying: () {
+ /// do something on playing
+ },
+ onError: (err) {
+ /// do somethin on error
+ },
+ isFile: state.file!.path.startsWith('/uploads'),
+ maxDuration: const Duration(seconds: 10),
+ ),
+ innerPadding: 0,
+ cornerRadius: 20,
+ circlesColor: Theme.of(context).colorScheme.primary,
+ activeSliderColor:
+ Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ));
+ }
+
+ AnimatedVisibility fileContainer() {
+ final state = context.watch();
+ String basename = '';
+ if (state.file != null) {
+ basename = p.basename(state.file!.path);
+ }
+ return AnimatedVisibility(
+ isVisible: state.file != null &&
+ !lookupMimeType(state.file!.path)!.startsWith('audio/'),
+ duration: DesignConfig.lowAnimationDuration,
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: DesignConfig.mediumBorderRadius,
+ color: Theme.of(context).colorScheme.border,
+ ),
+ padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
+ margin: const EdgeInsets.only(bottom: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.file_copy),
+ const SizedBox(
+ width: 12,
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: 260,
+ height: 24,
+ child: MarqueeText(
+ text: basename,
+ style: const TextStyle(fontSize: 14),
+ stop: const Duration(seconds: 3),
+ ),
+ ),
+ if (state.file != null)
+ FutureBuilder(
+ future: state.file!.main.length(),
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return const SizedBox();
+ }
+ return DidvanText(
+ 'File Size ${(snapshot.data! / 1000).round()} KB',
+ fontSize: 12,
+ );
+ })
+ ],
+ )
+ ],
+ ),
+ InkWell(
+ onTap: () {
+ state.file = null;
+ state.update();
+ },
+ child: const Icon(DidvanIcons.close_circle_solid))
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/views/ai/widgets/message_bar_btn.dart b/lib/views/ai/widgets/message_bar_btn.dart
new file mode 100644
index 0000000..d167f7c
--- /dev/null
+++ b/lib/views/ai/widgets/message_bar_btn.dart
@@ -0,0 +1,37 @@
+import 'package:didvan/config/theme_data.dart';
+import 'package:flutter/material.dart';
+
+class MessageBarBtn extends StatelessWidget {
+ final bool enable;
+ final IconData icon;
+ final Function()? click;
+ final Color? color;
+ const MessageBarBtn(
+ {Key? key,
+ required this.enable,
+ required this.icon,
+ this.click,
+ this.color})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: enable
+ ? color ?? Theme.of(context).colorScheme.primary
+ : Theme.of(context).colorScheme.border),
+ child: InkWell(
+ onTap: click,
+ child: Icon(
+ icon,
+ size: 18,
+ color: enable ? Theme.of(context).colorScheme.white : null,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/views/ai/widgets/voice_message_view.dart b/lib/views/ai/widgets/voice_message_view.dart
new file mode 100644
index 0000000..02f124f
--- /dev/null
+++ b/lib/views/ai/widgets/voice_message_view.dart
@@ -0,0 +1,294 @@
+// ignore_for_file: implementation_imports, unused_element
+
+import 'package:didvan/constants/app_icons.dart';
+import 'package:didvan/views/ai/ai_chat_state.dart';
+import 'package:didvan/views/ai/widgets/message_bar_btn.dart';
+import 'package:flutter/material.dart';
+import 'package:mime/mime.dart';
+import 'package:provider/provider.dart';
+import 'package:voice_message_package/src/helpers/play_status.dart';
+import 'package:voice_message_package/src/helpers/utils.dart';
+import 'package:voice_message_package/src/voice_controller.dart';
+import 'package:voice_message_package/src/widgets/noises.dart';
+import 'package:voice_message_package/src/widgets/play_pause_button.dart';
+
+/// A widget that displays a voice message view with play/pause functionality.
+///
+/// The [VoiceMessageView] widget is used to display a voice message with customizable appearance and behavior.
+/// It provides a play/pause button, a progress slider, and a counter for the remaining time.
+/// The appearance of the widget can be customized using various properties such as background color, slider color, and text styles.
+///
+class MyVoiceMessageView extends StatelessWidget {
+ const MyVoiceMessageView(
+ {Key? key,
+ required this.controller,
+ this.backgroundColor = Colors.white,
+ this.activeSliderColor = Colors.red,
+ this.notActiveSliderColor,
+ this.circlesColor = Colors.red,
+ this.innerPadding = 12,
+ this.cornerRadius = 20,
+ // this.playerWidth = 170,
+ this.size = 38,
+ this.refreshIcon = const Icon(
+ Icons.refresh,
+ color: Colors.white,
+ ),
+ this.pauseIcon = const Icon(
+ Icons.pause_rounded,
+ color: Colors.white,
+ ),
+ this.playIcon = const Icon(
+ Icons.play_arrow_rounded,
+ color: Colors.white,
+ ),
+ this.stopDownloadingIcon = const Icon(
+ Icons.close,
+ color: Colors.white,
+ ),
+ this.playPauseButtonDecoration,
+ this.circlesTextStyle = const TextStyle(
+ color: Colors.white,
+ fontSize: 10,
+ fontWeight: FontWeight.bold,
+ ),
+ this.counterTextStyle = const TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w500,
+ ),
+ this.playPauseButtonLoadingColor = Colors.white})
+ : super(key: key);
+
+ /// The controller for the voice message view.
+ final VoiceController controller;
+
+ /// The background color of the voice message view.
+ final Color backgroundColor;
+
+ ///
+ final Color circlesColor;
+
+ /// The color of the active slider.
+ final Color activeSliderColor;
+
+ /// The color of the not active slider.
+ final Color? notActiveSliderColor;
+
+ /// The text style of the circles.
+ final TextStyle circlesTextStyle;
+
+ /// The text style of the counter.
+ final TextStyle counterTextStyle;
+
+ /// The padding between the inner content and the outer container.
+ final double innerPadding;
+
+ /// The corner radius of the outer container.
+ final double cornerRadius;
+
+ /// The size of the play/pause button.
+ final double size;
+
+ /// The refresh icon of the play/pause button.
+ final Widget refreshIcon;
+
+ /// The pause icon of the play/pause button.
+ final Widget pauseIcon;
+
+ /// The play icon of the play/pause button.
+ final Widget playIcon;
+
+ /// The stop downloading icon of the play/pause button.
+ final Widget stopDownloadingIcon;
+
+ /// The play Decoration of the play/pause button.
+ final Decoration? playPauseButtonDecoration;
+
+ /// The loading Color of the play/pause button.
+ final Color playPauseButtonLoadingColor;
+
+ @override
+
+ /// Build voice message view.
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ final color = circlesColor;
+ final newTHeme = theme.copyWith(
+ sliderTheme: SliderThemeData(
+ trackShape: CustomTrackShape(),
+ thumbShape: SliderComponentShape.noThumb,
+ minThumbSeparation: 0,
+ ),
+ splashColor: Colors.transparent,
+ );
+
+ final state = context.read();
+
+ return Container(
+ width: 160 + (controller.noiseCount * .72.w()),
+ padding: EdgeInsets.all(innerPadding),
+ decoration: BoxDecoration(
+ color: backgroundColor,
+ borderRadius: BorderRadius.circular(cornerRadius),
+ ),
+ child: ValueListenableBuilder(
+ /// update ui when change play status
+ valueListenable: controller.updater,
+ builder: (context, value, child) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const SizedBox(width: 12),
+
+ /// play pause button
+ PlayPauseButton(
+ controller: controller,
+ color: color,
+ loadingColor: playPauseButtonLoadingColor,
+ size: size,
+ refreshIcon: refreshIcon,
+ pauseIcon: pauseIcon,
+ playIcon: playIcon,
+ stopDownloadingIcon: stopDownloadingIcon,
+ buttonDecoration: playPauseButtonDecoration,
+ ),
+ const SizedBox(width: 12),
+
+ ///
+ Text(controller.remindingTime, style: counterTextStyle),
+
+ const SizedBox(width: 12),
+
+ /// slider & noises
+ Expanded(
+ child: _noises(newTHeme),
+ ),
+ const SizedBox(width: 12),
+
+ ///
+
+ /// speed button
+ // _changeSpeedButton(color),
+
+ ///
+ if (state.file != null &&
+ (lookupMimeType(state.file!.path)?.startsWith('audio/') ??
+ false))
+ MessageBarBtn(
+ enable: true,
+ icon: DidvanIcons.trash_solid,
+ color: Theme.of(context).colorScheme.error,
+ click: () async {
+ state.file = null;
+ state.update();
+ }),
+ const SizedBox(width: 12),
+ ],
+ );
+ },
+ ),
+ );
+ }
+
+ SizedBox _noises(ThemeData newTHeme) => SizedBox(
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ /// noises
+ Noises(
+ rList: controller.randoms!,
+ activeSliderColor: activeSliderColor,
+ ),
+
+ /// slider
+ AnimatedBuilder(
+ animation: CurvedAnimation(
+ parent: controller.animController,
+ curve: Curves.ease,
+ ),
+ builder: (BuildContext context, Widget? child) {
+ return Positioned(
+ left: controller.animController.value,
+ child: Container(
+ width: controller.noiseWidth,
+ height: 6.w(),
+ color:
+ notActiveSliderColor ?? backgroundColor.withOpacity(.4),
+ ),
+ );
+ },
+ ),
+ Opacity(
+ opacity: 0,
+ child: Container(
+ width: controller.noiseWidth,
+ color: Colors.transparent.withOpacity(1),
+ child: Theme(
+ data: newTHeme,
+ child: Slider(
+ value: controller.currentMillSeconds,
+ max: controller.maxMillSeconds,
+ onChangeStart: controller.onChangeSliderStart,
+ onChanged: controller.onChanging,
+ onChangeEnd: (value) {
+ controller.onSeek(
+ Duration(milliseconds: value.toInt()),
+ );
+ controller.play();
+ },
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+
+ Transform _changeSpeedButton(Color color) => Transform.translate(
+ offset: const Offset(0, -7),
+ child: GestureDetector(
+ onTap: () {
+ controller.changeSpeed();
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
+ decoration: BoxDecoration(
+ color: color,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ controller.speed.playSpeedStr,
+ style: circlesTextStyle,
+ ),
+ ),
+ ),
+ );
+}
+
+///
+/// A custom track shape for a slider that is rounded rectangular in shape.
+/// Extends the [RoundedRectSliderTrackShape] class.
+class CustomTrackShape extends RoundedRectSliderTrackShape {
+ @override
+
+ /// Returns the preferred rectangle for the voice message view.
+ ///
+ /// The preferred rectangle is calculated based on the current state and layout
+ /// of the voice message view. It represents the area where the view should be
+ /// displayed on the screen.
+ ///
+ /// Returns a [Rect] object representing the preferred rectangle.
+ Rect getPreferredRect({
+ required RenderBox parentBox,
+ Offset offset = Offset.zero,
+ required SliderThemeData sliderTheme,
+ bool isEnabled = false,
+ bool isDiscrete = false,
+ }) {
+ const double trackHeight = 10;
+ final double trackLeft = offset.dx,
+ trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
+ final double trackWidth = parentBox.size.width;
+ return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
+ }
+}
diff --git a/lib/views/direct/direct_state.dart b/lib/views/direct/direct_state.dart
index 1b97b8e..86a5744 100644
--- a/lib/views/direct/direct_state.dart
+++ b/lib/views/direct/direct_state.dart
@@ -10,10 +10,11 @@ import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_vibrate/flutter_vibrate.dart';
+import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
class DirectState extends CoreProvier {
- final _recorder = Record();
+ final _recorder = AudioRecorder();
final List messages = [];
late final int typeId;
final Map> dailyMessages = {};
@@ -59,7 +60,10 @@ class DirectState extends CoreProvier {
Vibrate.feedback(FeedbackType.medium);
}
isRecording = true;
- _recorder.start();
+ Directory tempDir = await getTemporaryDirectory();
+ _recorder.start(const RecordConfig(),
+ path:
+ '${tempDir.path}/${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a');
notifyListeners();
}
diff --git a/lib/views/home/home.dart b/lib/views/home/home.dart
index b989425..9ea6046 100644
--- a/lib/views/home/home.dart
+++ b/lib/views/home/home.dart
@@ -1,26 +1,40 @@
// ignore_for_file: deprecated_member_use
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:didvan/config/design_config.dart';
+import 'package:didvan/config/theme_data.dart';
+import 'package:didvan/constants/app_icons.dart';
+import 'package:didvan/constants/assets.dart';
+import 'package:didvan/main.dart';
+import 'package:didvan/models/ai/ai_chat_args.dart';
import 'package:didvan/models/notification_message.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/providers/theme.dart';
+import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/services/notification/notification_service.dart';
import 'package:didvan/utils/action_sheet.dart';
-import 'package:didvan/views/ai/history_ai_chat_page.dart';
+import 'package:didvan/views/ai/ai.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/home/categories/categories_page.dart';
import 'package:didvan/views/home/main/main_page.dart';
import 'package:didvan/views/home/home_state.dart';
import 'package:didvan/views/home/new_statistic/new_statistic.dart';
import 'package:didvan/views/home/search/search.dart';
+import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/logo_app_bar.dart';
import 'package:didvan/views/widgets/didvan/bnb.dart';
+import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../services/app_home_widget/home_widget_repository.dart';
+final GlobalKey homeScaffKey = GlobalKey();
+
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@@ -42,6 +56,9 @@ class _HomeState extends State
state.tabController = _tabController;
_tabController.addListener(() {
state.currentPageIndex = _tabController.index;
+ if (_tabController.index == 2) {
+ context.read().getChats();
+ }
});
Future.delayed(Duration.zero, () {
@@ -64,7 +81,420 @@ class _HomeState extends State
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: const LogoAppBar(),
+ key: homeScaffKey,
+ appBar: LogoAppBar(
+ canSearch: context.watch().tabController.index != 2,
+ ),
+ resizeToAvoidBottomInset: false,
+ drawer: context.watch().tabController.index == 2
+ ? Drawer(
+ child: Consumer(
+ builder: (context, state, child) {
+ return Column(
+ children: [
+ const SizedBox(
+ height: 8,
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 20.0, top: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ InkWell(
+ onTap: () =>
+ homeScaffKey.currentState!.closeDrawer(),
+ child: const Icon(
+ DidvanIcons.close_regular,
+ ),
+ )
+ ],
+ ),
+ ),
+ Icon(
+ DidvanIcons.ai_solid,
+ size: MediaQuery.sizeOf(context).width / 5,
+ ),
+ const DidvanText('هوشان'),
+ const SizedBox(
+ height: 24,
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Column(
+ children: [
+ drawerBtn(
+ icon: Icons.handshake_rounded,
+ text: 'ساخت دستیار شخصی',
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ enable: false),
+ const DidvanDivider(),
+ drawerBtn(
+ icon: CupertinoIcons.doc_text_search,
+ text: 'جستجو در مدلها',
+ click: () {
+ ActionSheetUtils.botsDialogSelect(
+ context: context, state: state);
+ homeScaffKey.currentState!
+ .closeDrawer();
+ },
+ enable: false),
+ const DidvanDivider(),
+ drawerBtn(
+ icon: DidvanIcons.chats_regular,
+ text: 'تاریخچه همه گفتگوها',
+ label: 'حذف همه',
+ click: () {
+ Navigator.of(context)
+ .pushNamed(Routes.aiHistory);
+ },
+ labelClick: state.chats.isEmpty
+ ? null
+ : () async {
+ ActionSheetUtils.context =
+ context;
+ await ActionSheetUtils.openDialog(
+ data: ActionSheetData(
+ onConfirmed: () async {
+ await state
+ .deleteAllChat();
+ },
+ content: Column(
+ children: [
+ Row(
+ crossAxisAlignment:
+ CrossAxisAlignment
+ .center,
+ children: [
+ Icon(
+ DidvanIcons
+ .trash_solid,
+ color: Theme.of(
+ context)
+ .colorScheme
+ .error,
+ ),
+ const SizedBox(
+ width: 8,
+ ),
+ DidvanText(
+ 'پاک کردن همه گفتوگوها',
+ color: Theme.of(
+ context)
+ .colorScheme
+ .error,
+ fontSize: 20,
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 12,
+ ),
+ const DidvanText(
+ 'آیا از پاک کردن تمامی گفتوگوهای انجام شده با هوشان اطمینان دارید؟'),
+ ],
+ )));
+ },
+ ),
+ const SizedBox(
+ height: 12,
+ ),
+ // SearchField(
+ // title: 'title',
+ // onChanged: (value) {},
+ // focusNode: FocusNode()),
+ // SizedBox(
+ // height: 12,
+ // ),
+ Expanded(
+ child: state.chats.isEmpty
+ ? Padding(
+ padding:
+ const EdgeInsets.all(12.0),
+ child: EmptyState(
+ asset: Assets.emptyResult,
+ title: 'لیست خالی است',
+ ),
+ )
+ : state.loadingdeleteAll
+ ? Center(
+ child: Image.asset(
+ Assets.loadingAnimation,
+ width: 60,
+ height: 60,
+ ),
+ )
+ : ListView.builder(
+ shrinkWrap: true,
+ itemCount: state.chats.length,
+ padding: const EdgeInsets
+ .symmetric(
+ horizontal: 12),
+ physics:
+ const BouncingScrollPhysics(),
+ itemBuilder:
+ (context, index) {
+ final chat =
+ state.chats[index];
+ TextEditingController
+ title =
+ TextEditingController(
+ text: chat.title);
+
+ return Padding(
+ padding: const EdgeInsets
+ .symmetric(
+ vertical: 8.0),
+ child: InkWell(
+ onTap: () {
+ navigatorKey
+ .currentState!
+ .pushNamed(
+ Routes.aiChat,
+ arguments: AiChatArgs(
+ bot: chat
+ .bot!,
+ chat:
+ chat));
+ },
+ child: Row(
+ children: [
+ ClipOval(
+ child:
+ CachedNetworkImage(
+ imageUrl: chat
+ .bot!.image
+ .toString(),
+ width: 24,
+ height: 24,
+ )),
+ const SizedBox(
+ width: 12,
+ ),
+ Expanded(
+ child: chat.isEditing !=
+ null &&
+ chat
+ .isEditing!
+ ? TextFormField(
+ controller:
+ title,
+ style: const TextStyle(
+ fontSize:
+ 12),
+ textAlignVertical:
+ TextAlignVertical
+ .bottom,
+ maxLines:
+ 1,
+ decoration:
+ const InputDecoration(
+ isDense:
+ true,
+ contentPadding: EdgeInsets.symmetric(
+ vertical:
+ 5,
+ horizontal:
+ 10),
+ border:
+ OutlineInputBorder(),
+ ))
+ : Text(
+ chat.title
+ .toString(),
+ overflow:
+ TextOverflow
+ .ellipsis,
+ maxLines:
+ 1,
+ ),
+ ),
+ const SizedBox(
+ width: 24,
+ ),
+ Row(
+ children: [
+ chat.isEditing !=
+ null &&
+ chat
+ .isEditing! &&
+ state
+ .loadingchangeTitle
+ ? const SizedBox(
+ width:
+ 12,
+ height:
+ 12,
+ child:
+ CircularProgressIndicator())
+ : InkWell(
+ onTap:
+ () async {
+ if (title
+ .text
+ .isNotEmpty) {
+ await state.changeNameChat(
+ chat.id!,
+ index,
+ title.text);
+ title.clear();
+ }
+ if (chat.isEditing !=
+ null) {
+ chat.isEditing =
+ !chat.isEditing!;
+ state.update();
+ return;
+ }
+ chat.isEditing =
+ true;
+
+ state
+ .update();
+ },
+ child:
+ Icon(
+ chat.isEditing != null && chat.isEditing!
+ ? Icons.save
+ : Icons.edit_outlined,
+ size:
+ 18,
+ ),
+ ),
+ const SizedBox(
+ width: 8,
+ ),
+ PopupMenuButton(
+ onSelected:
+ (value) async {
+ switch (
+ value) {
+ case 'حذف پیام':
+ await state.deleteChat(
+ chat.id!,
+ index);
+ break;
+
+ case 'آرشیو':
+ await state.archivedChat(
+ chat.id!,
+ index);
+ await state
+ .getChats();
+ break;
+ default:
+ }
+
+ state
+ .update();
+ },
+ itemBuilder:
+ (BuildContext
+ context) {
+ return [
+ AiMessageBar.popUpBtns(
+ value:
+ 'حذف پیام',
+ icon: DidvanIcons
+ .trash_regular,
+ color: Theme.of(context)
+ .colorScheme
+ .error,
+ height:
+ 24,
+ size:
+ 12),
+ AiMessageBar
+ .popUpBtns(
+ value:
+ 'آرشیو',
+ icon: Icons
+ .folder_copy,
+ height:
+ 24,
+ size:
+ 12,
+ ),
+ ];
+ },
+ offset:
+ const Offset(
+ 0, 0),
+ position:
+ PopupMenuPosition
+ .under,
+ useRootNavigator:
+ true,
+ child:
+ const Icon(
+ Icons
+ .more_vert,
+ size: 18,
+ ),
+ ),
+ ],
+ )
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ // SizedBox(
+ // height: 12,
+ // ),
+ // Text('نمایش قدیمیترها')
+ ],
+ ),
+ ),
+ Column(
+ children: [
+ const DidvanDivider(),
+ drawerBtn(
+ icon: Icons.folder_copy_outlined,
+ text: 'گفتوگوهای آرشیو شده',
+ click: () {
+ Navigator.of(context).pushNamed(
+ Routes.aiHistory,
+ arguments: true);
+ },
+ ),
+ const SizedBox(
+ height: 12,
+ ),
+ drawerBtn(
+ icon: DidvanIcons.support_regular,
+ text: 'پیام به پشتیبانی',
+ click: () {
+ Navigator.of(context).pushNamed(
+ Routes.direct,
+ arguments: {
+ 'type': 'پشتیبانی اپلیکیشن'
+ },
+ );
+ },
+ ),
+ ],
+ )
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(
+ height: 32,
+ )
+ ],
+ );
+ },
+ ),
+ )
+ : null,
body: WillPopScope(
onWillPop: () async {
if (context.read().tabController.index == 0) {
@@ -101,7 +531,7 @@ class _HomeState extends State
children: const [
MainPage(),
CategoriesPage(),
- HistoryAiChatPage(),
+ Ai(),
NewStatistic(),
//Statistic(),
// Bookmarks(),
@@ -125,4 +555,58 @@ class _HomeState extends State
),
);
}
+
+ Widget drawerBtn(
+ {final CrossAxisAlignment? crossAxisAlignment,
+ required final IconData icon,
+ required final String text,
+ final bool enable = true,
+ final String? label,
+ final Function()? labelClick,
+ final Function()? click}) {
+ return InkWell(
+ onTap: enable ? click : null,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ crossAxisAlignment:
+ crossAxisAlignment ?? CrossAxisAlignment.center,
+ children: [
+ Icon(
+ icon,
+ color: enable
+ ? null
+ : Theme.of(context).colorScheme.disabledText,
+ ),
+ const SizedBox(
+ width: 8,
+ ),
+ Column(
+ children: [
+ DidvanText(text,
+ color: enable
+ ? null
+ : Theme.of(context).colorScheme.disabledText),
+ // if (!enable) Text('در حال توسعه ...')
+ ],
+ )
+ ],
+ ),
+ if (label != null)
+ InkWell(
+ onTap: labelClick,
+ child: DidvanText(
+ label,
+ color: Theme.of(context).colorScheme.primary,
+ fontSize: 12,
+ ),
+ )
+ ],
+ ),
+ ),
+ );
+ }
}
diff --git a/lib/views/profile/profile.dart b/lib/views/profile/profile.dart
index 2fda0e0..b70b858 100644
--- a/lib/views/profile/profile.dart
+++ b/lib/views/profile/profile.dart
@@ -176,8 +176,8 @@ class _ProfilePageState extends State {
.title,
))),
AnimatedVisibility(
- isVisible: state.showThemes,
duration: DesignConfig.lowAnimationDuration,
+ isVisible: state.showThemes,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12.0),
@@ -242,6 +242,7 @@ class _ProfilePageState extends State {
AnimatedVisibility(
isVisible: state.showContactUs,
duration: DesignConfig.lowAnimationDuration,
+ fadeMode: FadeMode.vertical,
child: Padding(
padding:
const EdgeInsets.only(right: 8.0, top: 8),
diff --git a/lib/views/widgets/didvan/bnb.dart b/lib/views/widgets/didvan/bnb.dart
index 06eb1ca..17e4fe9 100644
--- a/lib/views/widgets/didvan/bnb.dart
+++ b/lib/views/widgets/didvan/bnb.dart
@@ -59,7 +59,7 @@ class DidvanBNB extends StatelessWidget {
),
_NavBarItem(
isSelected: currentTabIndex == 2,
- title: 'هوش مصنوعی',
+ title: 'هوشان',
selectedIcon: DidvanIcons.ai_solid,
unselectedIcon: DidvanIcons.ai_regular,
onTap: () => onTabChanged(2),
diff --git a/lib/views/widgets/logo_app_bar.dart b/lib/views/widgets/logo_app_bar.dart
index 7cdcb6e..ea3b626 100644
--- a/lib/views/widgets/logo_app_bar.dart
+++ b/lib/views/widgets/logo_app_bar.dart
@@ -18,7 +18,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class LogoAppBar extends StatelessWidget implements PreferredSizeWidget {
- const LogoAppBar({Key? key}) : super(key: key);
+ final bool canSearch;
+ const LogoAppBar({Key? key, this.canSearch = true}) : super(key: key);
@override
Size get preferredSize => const Size(double.infinity, 144);
@@ -28,7 +29,7 @@ class LogoAppBar extends StatelessWidget implements PreferredSizeWidget {
final state = context.read();
final MediaQueryData d = MediaQuery.of(context);
return Container(
- height: 144,
+ height: canSearch ? 144 : 100,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(20)),
color: Theme.of(context).colorScheme.surface,
@@ -100,23 +101,24 @@ class LogoAppBar extends StatelessWidget implements PreferredSizeWidget {
],
),
const SizedBox(height: 16),
- Consumer(
- builder: (context, state, child) => SearchField(
- key: state.search.isEmpty ? ValueKey(state.search) : null,
- value: state.search,
- title: 'دیدوان',
- onChanged: (value) => _onChanged(value, context),
- focusNode: state.searchFieldFocusNode,
- onFilterButtonPressed: () => _showFilterBottomSheet(context),
- isFiltered: state.filtering,
- onGoBack: state.filtering
- ? () {
- state.resetFilters(false);
- FocusScope.of(context).unfocus();
- }
- : null,
+ if (canSearch)
+ Consumer(
+ builder: (context, state, child) => SearchField(
+ key: state.search.isEmpty ? ValueKey(state.search) : null,
+ value: state.search,
+ title: 'دیدوان',
+ onChanged: (value) => _onChanged(value, context),
+ focusNode: state.searchFieldFocusNode,
+ onFilterButtonPressed: () => _showFilterBottomSheet(context),
+ isFiltered: state.filtering,
+ onGoBack: state.filtering
+ ? () {
+ state.resetFilters(false);
+ FocusScope.of(context).unfocus();
+ }
+ : null,
+ ),
),
- ),
],
),
);
diff --git a/pubspec.lock b/pubspec.lock
index 259678e..3ec0782 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -726,7 +726,7 @@ packages:
source: hosted
version: "1.11.0"
mime:
- dependency: transitive
+ dependency: "direct main"
description:
name: mime
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
@@ -766,7 +766,7 @@ packages:
source: hosted
version: "3.0.1"
path:
- dependency: transitive
+ dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
@@ -937,50 +937,58 @@ packages:
dependency: "direct main"
description:
name: record
- sha256: f703397f5a60d9b2b655b3acc94ba079b2d9a67dc0725bdb90ef2fee2441ebf7
+ sha256: "4a5cf4d083d1ee49e0878823c4397d073f8eb0a775f31215d388e2bc47a9e867"
url: "https://pub.dev"
source: hosted
- version: "4.4.4"
+ version: "5.1.2"
+ record_android:
+ dependency: transitive
+ description:
+ name: record_android
+ sha256: d7af0b3119725a0f561817c72b5f5eca4d7a76d441deef519ae04e4824c0734c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.6"
+ record_darwin:
+ dependency: transitive
+ description:
+ name: record_darwin
+ sha256: fe90d302acb1f3cee1ade5df9c150ca5cee33b48d8cdf1cf433bf577d7f00134
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
record_linux:
dependency: transitive
description:
name: record_linux
- sha256: "348db92c4ec1b67b1b85d791381c8c99d7c6908de141e7c9edc20dad399b15ce"
+ sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3"
url: "https://pub.dev"
source: hosted
- version: "0.4.1"
- record_macos:
- dependency: transitive
- description:
- name: record_macos
- sha256: d1d0199d1395f05e218207e8cacd03eb9dc9e256ddfe2cfcbbb90e8edea06057
- url: "https://pub.dev"
- source: hosted
- version: "0.2.2"
+ version: "0.7.2"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
- sha256: "7a2d4ce7ac3752505157e416e4e0d666a54b1d5d8601701b7e7e5e30bec181b4"
+ sha256: "11f8b03ea8a0e279b0e306571dbe0db0202c0b8e866495c9fa1ad2281d5e4c15"
url: "https://pub.dev"
source: hosted
- version: "0.5.0"
+ version: "1.1.0"
record_web:
- dependency: "direct main"
+ dependency: transitive
description:
name: record_web
- sha256: "219ffb4ca59b4338117857db56d3ffadbde3169bcaf1136f5f4d4656f4a2372d"
+ sha256: "0ef370d1e6553ad33c39dd03103b374e7861f3518b0533e64c94d73f988a5ffa"
url: "https://pub.dev"
source: hosted
- version: "0.5.0"
+ version: "1.1.0"
record_windows:
dependency: transitive
description:
name: record_windows
- sha256: "42d545155a26b20d74f5107648dbb3382dbbc84dc3f1adc767040359e57a1345"
+ sha256: e653555aa3fda168aded7c34e11bd82baf0c6ac84e7624553def3c77ffefd36f
url: "https://pub.dev"
source: hosted
- version: "0.7.1"
+ version: "1.0.3"
rive:
dependency: "direct main"
description:
@@ -1066,6 +1074,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
+ syncfusion_flutter_core:
+ dependency: transitive
+ description:
+ name: syncfusion_flutter_core
+ sha256: e17dcc7a1d0701e84d0a83c0040503cdcc6c72e44db0d733ab4c706dd5b8b9f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "25.2.7"
+ syncfusion_flutter_sliders:
+ dependency: transitive
+ description:
+ name: syncfusion_flutter_sliders
+ sha256: "842a452fd73fd61fbebbff72d726ffea4cdaacf088ca2738aeaf7f4454b3861e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "25.2.7"
synchronized:
dependency: transitive
description:
@@ -1282,6 +1306,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "13.0.0"
+ voice_message_package:
+ dependency: "direct main"
+ description:
+ name: voice_message_package
+ sha256: cd7a717751e60908d37624309c6995c1c62c1bab6b54f7d15c2d71289f0b0cb6
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.1"
wakelock_plus:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 08e41ba..139aca2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -51,8 +51,7 @@ dependencies:
carousel_slider: ^4.0.0
flutter_vibrate: ^1.3.0
universal_html: ^2.0.8
- record: ^4.4.3
- record_web: ^0.5.0
+ record: ^5.1.2
persian_datetime_picker: ^2.6.0
persian_number_utility: ^1.1.1
bot_toast: ^4.0.1
@@ -90,8 +89,12 @@ dependencies:
flutter_markdown: ^0.7.3+1
file_picker: ^8.0.5
marquee: ^2.2.3
+ voice_message_package: ^2.2.1
+ mime: ^1.0.2
+
# onesignal_flutter: ^3.5.0
+ path: any
dev_dependencies:
flutter_test:
sdk: flutter