houshvan version 1 -- 11/06/1403 -- Rhmn
This commit is contained in:
parent
5ec13a7a52
commit
23747f5a62
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ?? '');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,4 +109,11 @@ class MediaService {
|
|||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
static Future<FilePickerResult?> pickAudioFile() async {
|
||||
return await FilePicker.platform.pickFiles(
|
||||
type: FileType.audio,
|
||||
allowMultiple: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
74
pubspec.lock
74
pubspec.lock
|
|
@ -726,7 +726,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.11.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mime
|
||||
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
|
||||
|
|
@ -766,7 +766,7 @@ packages:
|
|||
source: hosted
|
||||
version: "3.0.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
|
|
@ -937,50 +937,58 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: record
|
||||
sha256: f703397f5a60d9b2b655b3acc94ba079b2d9a67dc0725bdb90ef2fee2441ebf7
|
||||
sha256: "4a5cf4d083d1ee49e0878823c4397d073f8eb0a775f31215d388e2bc47a9e867"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.4"
|
||||
version: "5.1.2"
|
||||
record_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_android
|
||||
sha256: d7af0b3119725a0f561817c72b5f5eca4d7a76d441deef519ae04e4824c0734c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.6"
|
||||
record_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_darwin
|
||||
sha256: fe90d302acb1f3cee1ade5df9c150ca5cee33b48d8cdf1cf433bf577d7f00134
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
record_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_linux
|
||||
sha256: "348db92c4ec1b67b1b85d791381c8c99d7c6908de141e7c9edc20dad399b15ce"
|
||||
sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
record_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_macos
|
||||
sha256: d1d0199d1395f05e218207e8cacd03eb9dc9e256ddfe2cfcbbb90e8edea06057
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
version: "0.7.2"
|
||||
record_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_platform_interface
|
||||
sha256: "7a2d4ce7ac3752505157e416e4e0d666a54b1d5d8601701b7e7e5e30bec181b4"
|
||||
sha256: "11f8b03ea8a0e279b0e306571dbe0db0202c0b8e866495c9fa1ad2281d5e4c15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "1.1.0"
|
||||
record_web:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_web
|
||||
sha256: "219ffb4ca59b4338117857db56d3ffadbde3169bcaf1136f5f4d4656f4a2372d"
|
||||
sha256: "0ef370d1e6553ad33c39dd03103b374e7861f3518b0533e64c94d73f988a5ffa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "1.1.0"
|
||||
record_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_windows
|
||||
sha256: "42d545155a26b20d74f5107648dbb3382dbbc84dc3f1adc767040359e57a1345"
|
||||
sha256: e653555aa3fda168aded7c34e11bd82baf0c6ac84e7624553def3c77ffefd36f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.1"
|
||||
version: "1.0.3"
|
||||
rive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1066,6 +1074,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
syncfusion_flutter_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: e17dcc7a1d0701e84d0a83c0040503cdcc6c72e44db0d733ab4c706dd5b8b9f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "25.2.7"
|
||||
syncfusion_flutter_sliders:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_sliders
|
||||
sha256: "842a452fd73fd61fbebbff72d726ffea4cdaacf088ca2738aeaf7f4454b3861e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "25.2.7"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1282,6 +1306,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
voice_message_package:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: voice_message_package
|
||||
sha256: cd7a717751e60908d37624309c6995c1c62c1bab6b54f7d15c2d71289f0b0cb6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
wakelock_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue