didvan-app/lib/views/ai/ai_chat_state.dart

349 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lib/views/ai/ai_chat_state.dart
// ignore_for_file: body_might_complete_normally_catch_error, avoid_print
import 'dart:async';
import 'dart:convert';
import 'package:didvan/main.dart';
import 'package:didvan/models/ai/files_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
import 'package:didvan/models/ai/bots_model.dart';
import 'package:didvan/models/ai/chats_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:didvan/views/ai/history_ai_chat_state.dart';
class AiChatState extends CoreProvier {
ValueNotifier<Stream<String>> messageOnstream =
ValueNotifier(const Stream<String>.empty().asBroadcastStream());
List<MessageModel> messages = [];
bool onResponsing = false;
bool loading = false;
bool isEdite = false;
ValueNotifier<bool> changingPlaceHolder = ValueNotifier(false);
final ScrollController scrollController = ScrollController();
int? chatId;
ChatsModel? chat;
FilesModel? file;
bool isRecorded = false;
TextEditingController message = TextEditingController();
// --- ADDED ---
List<String> suggestedQuestions = [];
bool isLoadingQuestions = true;
// --- END ADDED ---
Future<void> _scrolledEnd() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await scrollController.animateTo(
scrollController.position.minScrollExtent,
duration: const Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
});
}
Future<void> _onError(e) async {
onResponsing = false;
messages.last.prompts.removeLast();
messages.last.prompts.last =
messages.last.prompts.last.copyWith(error: true);
messageOnstream.value = const Stream.empty();
await ActionSheetUtils(navigatorKey.currentContext!).showAlert(AlertData(
message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
update();
print("error is ${e.toString()}");
}
Future<RequestService> getChatId() async {
final service = RequestService(
RequestHelper.aiChatId(),
);
await service.httpGet();
if (service.isSuccess) {
final id = service.result['id'];
chatId = id;
chat ??= ChatsModel(id: chatId);
}
return service;
}
// --- ADDED ---
Future<void> fetchSuggestedQuestions() async {
isLoadingQuestions = true;
suggestedQuestions.clear();
update();
// 1. RequestHelper فقط URL string را برمی‌گرداند
final service = RequestService(
RequestHelper.aiSuggestedQuestions(),
// 2. بدنه درخواست خالی است و هدر Authorization خودکار اضافه می‌شود
);
// 3. متد post() را (به جای httpPost) فراخوانی می‌کنیم
await service.post();
if (service.isSuccess) {
// 4. از متد data() برای خواندن نتیجه استفاده می‌کنیم
final List<dynamic> questionsList = service.data('Questions');
if (questionsList.isNotEmpty) {
suggestedQuestions = questionsList.map((q) => q.toString()).toList();
}
isLoadingQuestions = false;
update();
} else {
// 5. از errorMessage برای نمایش خطا استفاده می‌کنیم
print("Error fetching suggested questions: ${service.errorMessage}");
isLoadingQuestions = false;
update();
}
}
// --- END ADDED ---
Future<void> getAllMessages(int chatId) async {
loading = true;
onResponsing = true;
update();
final service = RequestService(
RequestHelper.aiAChat(chatId),
);
await service.httpGet();
if (service.isSuccess) {
messages.clear();
final ChatsModel allMessages =
ChatsModel.fromJson(service.result['chat']);
for (var i = 0; i < allMessages.prompts!.length; i++) {
final prompt = allMessages.prompts![i];
if (messages.isNotEmpty &&
DateTime.parse(messages.last.prompts.last.createdAt.toString())
.toPersianDateStr()
.contains(DateTime.parse(prompt.createdAt.toString())
.toPersianDateStr())) {
messages.last.prompts.add(prompt);
} else {
messages.add(MessageModel(
dateTime: prompt.createdAt.toString(), prompts: [prompt]));
}
}
appState = AppState.idle;
loading = false;
onResponsing = false;
update();
return;
}
appState = AppState.failed;
loading = false;
onResponsing = false;
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;
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, bool isAssistants) async {
onResponsing = true;
final uploadedFile = file;
file = null;
update();
String message = messages.last.prompts.last.text!;
messages.last.prompts.add(Prompts(
finished: false,
error: false,
text: '',
role: 'bot',
createdAt: DateTime.now()
.subtract(const Duration(minutes: 210))
.toIso8601String()));
update();
await _scrolledEnd();
// تعیین URL بر اساس bot ID
String url;
if (bot.id == 100) {
// برای Aisummery از endpoint خاص استفاده می‌کنیم
url = '/100/aisummery';
} else {
// برای بقیه bot ها از endpoint عادی استفاده می‌کنیم
url = '${isAssistants ? '/user/${bot.responseType}' : ''}/${bot.id}/${bot.name}'
.toLowerCase();
}
final req = await AiApiService.initial(
url: url,
message: message,
chatId: chatId,
file: uploadedFile,
edite: isEdite);
file = null;
isRecorded = false;
update();
final res = await AiApiService().getResponse(req).catchError((e) {
_onError(e);
// return e;
});
String responseMessgae = '';
String dataMessgae = '';
update();
final r = res.listen((value) async {
var str = utf8.decode(value);
if (!kIsWeb) {
if (bot.responseType != 'text') {
responseMessgae += str.split('{{{').first;
}
if (str.contains('{{{')) {
dataMessgae += "{{{${str.split('{{{').last}";
update();
return;
}
}
responseMessgae += str;
if (kIsWeb) {
try {
int startIndex = responseMessgae.indexOf('{{{');
String slicedText =
responseMessgae.substring(startIndex, responseMessgae.length);
dataMessgae = slicedText;
responseMessgae = responseMessgae.replaceAll(dataMessgae, '');
} catch (e) {
e.printError();
}
if (bot.responseType != 'text') {
responseMessgae = "${responseMessgae.split('.png').first}.png";
return;
}
}
messageOnstream.value = Stream.value(responseMessgae);
update();
});
r.onDone(() async {
if (chatId == null) {
final service = await getChatId();
navigatorKey.currentContext!.read<HistoryAiChatState>().getChats();
if (!service.isSuccess) {
_onError(null);
return;
}
}
onResponsing = false;
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;
}
messages.last.prompts.last = messages.last.prompts.last.copyWith(
finished: true,
text: bot.responseType != 'text' ? null : responseMessgae,
file: bot.responseType != 'text' ? responseMessgae : null,
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();
}
if (isEdite) {
messages.last.prompts.removeAt((messages.last.prompts.length - 1) - 2);
messages.last.prompts.removeAt((messages.last.prompts.length - 1) - 2);
isEdite = false;
}
update();
_scrolledEnd();
});
r.onError((e) => _onError(e));
}
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(navigatorKey.currentContext!).showAlert(AlertData(
message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
update();
}
Future<void> clearChat() async {
messages.clear();
chatId = null;
chat = null;
file = null;
message.clear();
isEdite = false;
onResponsing = false;
loading = false;
notifyListeners();
await Future.delayed(const Duration(milliseconds: 10));
}
}