houshvan version 1 -- 11/06/1403 -- Rhmn

This commit is contained in:
OkaykOrhmn 2024-09-01 16:53:46 +03:30
parent 5ec13a7a52
commit 23747f5a62
29 changed files with 3081 additions and 870 deletions

View File

@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application
android:icon="@mipmap/ic_launcher"

View File

@ -1,60 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>didvan</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>We need to access to the user gallery to add user profile photo</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access to the microphone to record audio file</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to access to the user gallery to add user profile photo</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UNNotificationServiceExtension</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).MyNotificationServiceExtension</string>
</array>
</dict>
</plist>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>didvan</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<false />
<key>LSRequiresIPhoneOS</key>
<true />
<key>NSCameraUsageDescription</key>
<string>We need to access to the user gallery to add user profile photo</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access to the microphone to record audio file</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to access to the user gallery to add user profile photo</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
<key>UNNotificationServiceExtension</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).MyNotificationServiceExtension</string>
</array>
<key>NSMicrophoneUsageDescription</key>
<string>Some message to describe why you need this permission</string>
</dict>
</plist>

View File

@ -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});
}

View File

@ -10,17 +10,20 @@ class ChatsModel {
String? updatedAt;
BotsModel? bot;
List<Prompts>? 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<String, dynamic> 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>? 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<String, dynamic> 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,
);
}
}

View File

@ -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);
}
}

View File

@ -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,
});
}

View File

@ -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 ?? '');
}

View File

@ -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';

View File

@ -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<http.MultipartRequest> 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<ByteStream> 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<Stream<List<int>>> 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');
}
}
}

View File

@ -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<String, int> 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 = <String, int>{};
// 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;
}
}

View File

@ -109,4 +109,11 @@ class MediaService {
);
return result;
}
static Future<FilePickerResult?> pickAudioFile() async {
return await FilePicker.platform.pickFiles(
type: FileType.audio,
allowMultiple: false,
);
}
}

View File

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

View File

@ -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<MapEntry<String, dynamic>> additions) {
String result = '';

View File

@ -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<void> 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<bool>(
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();

View File

@ -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<Ai> {
@override
void initState() {
final state = context.read<HistoryAiChatState>();
Future.delayed(
Duration.zero,
() {
// state.getChats();
state.getBots();
},
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Container();
return Consumer<HistoryAiChatState>(
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),
)),
)
],
);
},
);
}
}

View File

@ -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<AiChatPage> {
TextEditingController message = TextEditingController();
FocusNode focusNode = FocusNode();
@override
void initState() {
final state = context.read<AiChatState>();
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<AiChatPage> {
context.read<HistoryAiChatState>().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<HistoryAiChatState>().getChats();
navigatorKey.currentState!.pop();
},
)
],
),
body: Consumer<AiChatState>(
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<AiChatState>(
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<AiChatState>(
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) =>
<PopupMenuEntry>[
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<HistoryAiChatState>().getChats();
},
),
],
));
}),
),
);
}
AnimatedVisibility fileContainer(BuildContext context) {
final state = context.read<AiChatState>();
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<AiChatState>();
await state
.changePlaceHolder(placeholder.text);
ActionSheetUtils.pop();
},
content: ValueListenableBuilder<bool>(
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<dynamic> 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<AiChatPage> {
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<AiChatPage> {
? 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<AiChatPage> {
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width / 1.5),
child: message.finished != null && !message.finished!
? StreamBuilder<String>(
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<Stream<String>>(
valueListenable: state.messageOnstream,
builder: (context, value, child) {
return StreamBuilder<String>(
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<AiChatPage> {
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,
),
),
),
],
)
],
),
),

View File

@ -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<String> messageOnstream = const Stream.empty();
ValueNotifier<Stream<String>> messageOnstream =
ValueNotifier(const Stream.empty());
List<MessageModel> messages = [];
bool onResponsing = false;
bool loading = false;
ValueNotifier<bool> changingPlaceHolder = ValueNotifier(false);
final ScrollController scrollController = ScrollController();
int? chatId;
File? file;
ChatsModel? chat;
FilesModel? file;
Future<void> _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<void> 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<void> 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<void> 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<void> 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();
}
}

View File

@ -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<HistoryAiChatPage> {
final state = context.read<HistoryAiChatState>();
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<HistoryAiChatState>(
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<HistoryAiChatState>().getChats();
return true;
},
child: DidvanScaffold(
hidePlayer: true,
physics: const BouncingScrollPhysics(),
// floatingActionButton: openAiListBtn(context),
padding: EdgeInsets.zero,
scrollController: scrollController,
showSliversFirst: false,
slivers: [
Consumer<HistoryAiChatState>(
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<HistoryAiChatState>();
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<HistoryAiChatPage> {
DidvanText(
chat.bot!.name.toString(),
fontWeight: FontWeight.bold,
// fontSize: 18,
),
DidvanText(
DateTimeUtils.momentGenerator(
@ -126,10 +241,103 @@ class _HistoryAiChatPageState extends State<HistoryAiChatPage> {
],
),
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<HistoryAiChatPage> {
),
],
),
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<HistoryAiChatState>();
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<HistoryAiChatState>();
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<HistoryAiChatState>();
final state = context.read<HistoryAiChatState>();
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<HistoryAiChatState>();
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<bool>(
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<HistoryAiChatState>();
// final state = context.read<HistoryAiChatState>();
// 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 {

View File

@ -12,15 +12,20 @@ import 'package:flutter/cupertino.dart';
class HistoryAiChatState extends CoreProvier {
final List<ChatsModel> chats = [];
final List<int> chatsToDelete = [];
// final List<int> chatsToDelete = [];
final List<BotsModel> bots = [];
BotsModel? bot;
ValueNotifier<bool> loadingBots = ValueNotifier(false);
bool loadingchangeTitle = false;
bool loadingdeleteAll = false;
Timer? timer;
String search = '';
Future<void> getChats() async {
Future<void> 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<void> 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<void> 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<void> addChatToDelete() async {
final service = RequestService(RequestHelper.aiDeleteChats(),
body: {"ids": chatsToDelete});
await service.delete();
// Future<void> addChatToDelete() async {
// final service = RequestService(RequestHelper.aiDeleteChats(),
// body: {"ids": chatsToDelete});
// await service.delete();
// if (service.isSuccess) {
// final List<ChatsModel> 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<void> 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<ChatsModel> 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<void> 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<void> 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<bool> 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;
}
}

View File

@ -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<AiMessageBar> createState() => _AiMessageBarState();
static PopupMenuItem<dynamic> 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<AiMessageBar> {
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<int> _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<AiChatState>(
builder: (context, state, child) {
final historyState = context.read<HistoryAiChatState>();
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<RecordState>(
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<HistoryAiChatState>()
.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<AiChatState>();
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<AiChatState>();
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))
],
),
),
);
}
}

View File

@ -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,
),
),
);
}
}

View File

@ -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<AiChatState>();
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);
}
}

View File

@ -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<MessageData> messages = [];
late final int typeId;
final Map<String, List<int>> 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();
}

View File

@ -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<ScaffoldState> homeScaffKey = GlobalKey<ScaffoldState>();
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@ -42,6 +56,9 @@ class _HomeState extends State<Home>
state.tabController = _tabController;
_tabController.addListener(() {
state.currentPageIndex = _tabController.index;
if (_tabController.index == 2) {
context.read<HistoryAiChatState>().getChats();
}
});
Future.delayed(Duration.zero, () {
@ -64,7 +81,420 @@ class _HomeState extends State<Home>
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const LogoAppBar(),
key: homeScaffKey,
appBar: LogoAppBar(
canSearch: context.watch<HomeState>().tabController.index != 2,
),
resizeToAvoidBottomInset: false,
drawer: context.watch<HomeState>().tabController.index == 2
? Drawer(
child: Consumer<HistoryAiChatState>(
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 <PopupMenuEntry>[
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<HomeState>().tabController.index == 0) {
@ -101,7 +531,7 @@ class _HomeState extends State<Home>
children: const [
MainPage(),
CategoriesPage(),
HistoryAiChatPage(),
Ai(),
NewStatistic(),
//Statistic(),
// Bookmarks(),
@ -125,4 +555,58 @@ class _HomeState extends State<Home>
),
);
}
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,
),
)
],
),
),
);
}
}

View File

@ -176,8 +176,8 @@ class _ProfilePageState extends State<ProfilePage> {
.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<ProfilePage> {
AnimatedVisibility(
isVisible: state.showContactUs,
duration: DesignConfig.lowAnimationDuration,
fadeMode: FadeMode.vertical,
child: Padding(
padding:
const EdgeInsets.only(right: 8.0, top: 8),

View File

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

View File

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

View File

@ -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:

View File

@ -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