Compare commits
No commits in common. "e30b01a3bb682719efe14c15d23b0d082bc4e155" and "61718baa54cb3506702d53ec6abdec6e94620260" have entirely different histories.
e30b01a3bb
...
61718baa54
|
|
@ -19,18 +19,7 @@ import 'package:didvan/services/storage/storage.dart';
|
||||||
import 'package:didvan/utils/action_sheet.dart';
|
import 'package:didvan/utils/action_sheet.dart';
|
||||||
|
|
||||||
class UserProvider extends CoreProvier {
|
class UserProvider extends CoreProvier {
|
||||||
/// قبلاً `late User user` بود — اگر صفحهای قبل از کامل شدن
|
late User user;
|
||||||
/// `getUserInfo()` به `user` دسترسی میگرفت، `LateInitializationError`
|
|
||||||
/// میداد. حالا با یک User تهی initialize میشود تا UI placeholder
|
|
||||||
/// نشان دهد بدون کرش.
|
|
||||||
User user = const User(
|
|
||||||
id: 0,
|
|
||||||
username: null,
|
|
||||||
phoneNumber: '',
|
|
||||||
photo: null,
|
|
||||||
fullName: '',
|
|
||||||
email: null,
|
|
||||||
);
|
|
||||||
bool isAuthenticated = false;
|
bool isAuthenticated = false;
|
||||||
int _unreadMessageCount = 0;
|
int _unreadMessageCount = 0;
|
||||||
|
|
||||||
|
|
@ -129,49 +118,19 @@ class UserProvider extends CoreProvier {
|
||||||
Future<String?> setAndGetToken({String? newToken}) async {
|
Future<String?> setAndGetToken({String? newToken}) async {
|
||||||
try {
|
try {
|
||||||
if (newToken == null) {
|
if (newToken == null) {
|
||||||
// اگر access token معتبر است، همان را برمیگردانیم.
|
final cached = TokenStorage.accessToken;
|
||||||
if (TokenStorage.isAccessTokenValid) {
|
if (cached != null && cached.isNotEmpty) {
|
||||||
final cached = TokenStorage.accessToken;
|
RequestService.token = cached;
|
||||||
if (cached != null && cached.isNotEmpty) {
|
return cached;
|
||||||
RequestService.token = cached;
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// access token منقضی است؛ اگر refresh token داریم، تلاش به
|
|
||||||
// refresh قبل از ادامه. اگر refresh هم fail شد، session را پاک
|
|
||||||
// میکنیم تا splash کاربر را به login بفرستد.
|
|
||||||
if (TokenStorage.refreshToken != null &&
|
|
||||||
TokenStorage.refreshToken!.isNotEmpty) {
|
|
||||||
final ok = await KeycloakAuthService.instance.refresh();
|
|
||||||
if (ok) {
|
|
||||||
final fresh = TokenStorage.accessToken;
|
|
||||||
if (fresh != null && fresh.isNotEmpty) {
|
|
||||||
RequestService.token = fresh;
|
|
||||||
await StorageService.setValue(key: 'token', value: fresh);
|
|
||||||
return fresh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// refresh fail کرد → session تمام شده، کاربر باید مجدد login کند.
|
|
||||||
print(
|
|
||||||
'🔐 setAndGetToken: refresh failed, clearing session for re-login');
|
|
||||||
await TokenStorage.clear();
|
|
||||||
await StorageService.delete(key: 'token');
|
|
||||||
RequestService.token = null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// نه access معتبر داریم نه refresh — هیچ session فعالی نیست.
|
|
||||||
final token = await StorageService.getValue(key: 'token');
|
final token = await StorageService.getValue(key: 'token');
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null) RequestService.token = token;
|
||||||
RequestService.token = token;
|
return token;
|
||||||
return token;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
RequestService.token = newToken;
|
RequestService.token = newToken;
|
||||||
await StorageService.setValue(key: 'token', value: newToken);
|
await StorageService.setValue(key: 'token', value: newToken);
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('🔐 setAndGetToken error: $e');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,14 @@ import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
class AiApiService {
|
class AiApiService {
|
||||||
// backend جدید (api_gateway) — legacy روی api.didvan.app (Liara) برای
|
static const String baseUrl = 'https://api.didvan.app/ai';
|
||||||
// نسخههای قدیم اپ زنده میماند.
|
|
||||||
static const String baseUrl = 'https://api2.didvan.com/ai';
|
|
||||||
|
|
||||||
static Future<http.MultipartRequest> initial(
|
static Future<http.MultipartRequest> initial(
|
||||||
{required final String url,
|
{required final String url,
|
||||||
required final String message,
|
required final String message,
|
||||||
final int? chatId,
|
final int? chatId,
|
||||||
final FilesModel? file,
|
final FilesModel? file,
|
||||||
final bool? edite,
|
final bool? edite}) async {
|
||||||
final bool? webSearch}) async {
|
|
||||||
final headers = {
|
final headers = {
|
||||||
"Authorization": "Bearer ${await StorageService.getValue(key: 'token')}",
|
"Authorization": "Bearer ${await StorageService.getValue(key: 'token')}",
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
|
|
@ -36,12 +33,6 @@ class AiApiService {
|
||||||
if (edite != null) {
|
if (edite != null) {
|
||||||
request.fields['edit'] = edite.toString().toLowerCase();
|
request.fields['edit'] = edite.toString().toLowerCase();
|
||||||
}
|
}
|
||||||
// وقتی toggle «جستجو در وب» در UI روشن است، این فلگ به backend
|
|
||||||
// فرستاده میشود تا در request به kie.ai مدل را با tools:[web_search]
|
|
||||||
// فرا بخواند.
|
|
||||||
if (webSearch == true) {
|
|
||||||
request.fields['webSearch'] = 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
if (file.duration != null) {
|
if (file.duration != null) {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,6 @@ import 'package:didvan/models/requests/studio.dart';
|
||||||
class RequestHelper {
|
class RequestHelper {
|
||||||
static const String baseUrl = 'https://api.didvan.app';
|
static const String baseUrl = 'https://api.didvan.app';
|
||||||
static const String baseUrl2 = 'https://opportunity-threat.didvan.com';
|
static const String baseUrl2 = 'https://opportunity-threat.didvan.com';
|
||||||
|
|
||||||
/// Base URL برای backend جدید (api_gateway در ubuntu server).
|
|
||||||
/// AI endpoints هوشان به این میروند — `api.didvan.app` همچنان به Liara
|
|
||||||
/// (نسخهٔ legacy AI) میرود تا نسخهٔ قدیم اپ که در Play هست همچنان
|
|
||||||
/// کار کند.
|
|
||||||
static const String newApiBaseUrl = 'https://api2.didvan.com';
|
|
||||||
static const String storiesV1 = '$baseUrl2/api/v1/stories';
|
static const String storiesV1 = '$baseUrl2/api/v1/stories';
|
||||||
static const String storyActivity = '$baseUrl2/api/v1/stories/activity';
|
static const String storyActivity = '$baseUrl2/api/v1/stories/activity';
|
||||||
static const String _baseUserUrl = '$baseUrl/user';
|
static const String _baseUserUrl = '$baseUrl/user';
|
||||||
|
|
@ -220,37 +214,34 @@ class RequestHelper {
|
||||||
static String deleteComment(int id) => '$baseUrl/comment/$id';
|
static String deleteComment(int id) => '$baseUrl/comment/$id';
|
||||||
static String reportComment(int id) => '$baseUrl/comment/$id/report';
|
static String reportComment(int id) => '$baseUrl/comment/$id/report';
|
||||||
static String widgetNews() => '$baseUrl/user/widget';
|
static String widgetNews() => '$baseUrl/user/widget';
|
||||||
// === AI / هوشان endpoints — به backend جدید (api2.didvan.com) میروند ===
|
static String aiChats() => '$baseUrl/ai/chat/v2';
|
||||||
// نسخهٔ legacy روی api.didvan.app (Liara) هنوز برای اپهای قدیم در دسترس
|
static String aiArchived() => '$baseUrl/ai/chat/v2${_urlConcatGenerator([
|
||||||
// است؛ اپ جدید فقط با backend جدید کار میکند.
|
|
||||||
static String aiChats() => '$newApiBaseUrl/ai/chat/v2';
|
|
||||||
static String aiArchived() => '$newApiBaseUrl/ai/chat/v2${_urlConcatGenerator([
|
|
||||||
const MapEntry('archived', true),
|
const MapEntry('archived', true),
|
||||||
])}';
|
])}';
|
||||||
static String aiBots() => '$newApiBaseUrl/ai/bot/v2';
|
static String aiBots() => '$baseUrl/ai/bot/v2';
|
||||||
static String aiSearchBots(String q) =>
|
static String aiSearchBots(String q) =>
|
||||||
'$newApiBaseUrl/ai/bot${_urlConcatGenerator([
|
'$baseUrl/ai/bot${_urlConcatGenerator([
|
||||||
MapEntry('q', q),
|
MapEntry('q', q),
|
||||||
])}';
|
])}';
|
||||||
static String aiSearchChats(String q) =>
|
static String aiSearchChats(String q) =>
|
||||||
'$newApiBaseUrl/ai/chat${_urlConcatGenerator([
|
'$baseUrl/ai/chat${_urlConcatGenerator([
|
||||||
MapEntry('q', q),
|
MapEntry('q', q),
|
||||||
])}';
|
])}';
|
||||||
static String aiSearchArchived(String q) =>
|
static String aiSearchArchived(String q) =>
|
||||||
'$newApiBaseUrl/ai/chat${_urlConcatGenerator([
|
'$baseUrl/ai/chat${_urlConcatGenerator([
|
||||||
MapEntry('q', q),
|
MapEntry('q', q),
|
||||||
const MapEntry('archived', true),
|
const MapEntry('archived', true),
|
||||||
])}';
|
])}';
|
||||||
static String aiAChat(int id) => '$newApiBaseUrl/ai/chat/$id/v2';
|
static String aiAChat(int id) => '$baseUrl/ai/chat/$id/v2';
|
||||||
static String aiChatId() => '$newApiBaseUrl/ai/chat/id';
|
static String aiChatId() => '$baseUrl/ai/chat/id';
|
||||||
static String aiDeleteChats() => '$newApiBaseUrl/ai/chat';
|
static String aiDeleteChats() => '$baseUrl/ai/chat';
|
||||||
static String aiChangeChats(int id) => '$newApiBaseUrl/ai/chat/$id/title';
|
static String aiChangeChats(int id) => '$baseUrl/ai/chat/$id/title';
|
||||||
static String deleteChat(int id) => '$newApiBaseUrl/ai/chat/$id';
|
static String deleteChat(int id) => '$baseUrl/ai/chat/$id';
|
||||||
static String deleteMessage(int chatId, int messageId) =>
|
static String deleteMessage(int chatId, int messageId) =>
|
||||||
'$newApiBaseUrl/ai/chat/$chatId/message/$messageId';
|
'$baseUrl/ai/chat/$chatId/message/$messageId';
|
||||||
static String deleteAllChats() => '$newApiBaseUrl/ai/chat/all';
|
static String deleteAllChats() => '$baseUrl/ai/chat/all';
|
||||||
static String archivedChat(int id) => '$newApiBaseUrl/ai/chat/$id/archive';
|
static String archivedChat(int id) => '$baseUrl/ai/chat/$id/archive';
|
||||||
static String placeholder(int id) => '$newApiBaseUrl/ai/chat/$id/placeholder';
|
static String placeholder(int id) => '$baseUrl/ai/chat/$id/placeholder';
|
||||||
static String aiSummery() => '$baseUrl/ai/aisummery';
|
static String aiSummery() => '$baseUrl/ai/aisummery';
|
||||||
static String aiAudio() => '$baseUrl/ai/101/aiaudio';
|
static String aiAudio() => '$baseUrl/ai/101/aiaudio';
|
||||||
static String chartAnalysis() => '$baseUrl/ai/27/chart-analysis';
|
static String chartAnalysis() => '$baseUrl/ai/27/chart-analysis';
|
||||||
|
|
@ -303,6 +294,6 @@ class RequestHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
static String aiSuggestedQuestions() {
|
static String aiSuggestedQuestions() {
|
||||||
return '$newApiBaseUrl/ai/aiquestion';
|
return '$baseUrl/ai/aiquestion';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,8 +95,7 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
|
||||||
prompts: [widget.args.prompts!]));
|
prompts: [widget.args.prompts!]));
|
||||||
state.message.clear();
|
state.message.clear();
|
||||||
state.update();
|
state.update();
|
||||||
state.postMessage(widget.args.bot, widget.args.assistantsName != null,
|
state.postMessage(widget.args.bot, widget.args.assistantsName != null);
|
||||||
webSearch: _isSearchMode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,22 +146,27 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
|
||||||
final bots = context.read<HistoryAiChatState>().bots;
|
final bots = context.read<HistoryAiChatState>().bots;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
// قبلاً _geminiBot از bot id=35 با name=gemini دنبال میشد
|
_searchBot = bots.firstWhere((b) => b.id == 36);
|
||||||
// (الگوی legacy multi-model). در ساختار جدید Gemini یک bot
|
|
||||||
// مستقل با id=36 است.
|
|
||||||
try {
|
try {
|
||||||
_searchBot = bots.firstWhere((b) => b.id == 36);
|
_geminiBot = bots.firstWhere((b) =>
|
||||||
} catch (_) {
|
b.id == 35 &&
|
||||||
_searchBot = null;
|
(b.name?.toLowerCase().contains('gemini') ?? false));
|
||||||
}
|
|
||||||
try {
|
|
||||||
_geminiBot = bots.firstWhere(
|
|
||||||
(b) => (b.name?.toLowerCase().contains('gemini') ?? false));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_geminiBot = BotsModel(
|
_geminiBot = BotsModel(
|
||||||
id: 36,
|
id: 35,
|
||||||
name: 'gemini',
|
name: 'Gemini',
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
|
attachmentType: [
|
||||||
|
'audio',
|
||||||
|
'image',
|
||||||
|
'pdf',
|
||||||
|
'csv',
|
||||||
|
'doc',
|
||||||
|
'docx',
|
||||||
|
'xls',
|
||||||
|
'xlsx'
|
||||||
|
],
|
||||||
|
attachment: 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -294,8 +298,7 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
|
||||||
state.message.clear();
|
state.message.clear();
|
||||||
state.update();
|
state.update();
|
||||||
await state.postMessage(
|
await state.postMessage(
|
||||||
widget.args.bot, widget.args.assistantsName != null,
|
widget.args.bot, widget.args.assistantsName != null);
|
||||||
webSearch: _isSearchMode);
|
|
||||||
Future.delayed(const Duration(milliseconds: 100), () {
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
state.scrollController.animateTo(
|
state.scrollController.animateTo(
|
||||||
state.scrollController.position.maxScrollExtent,
|
state.scrollController.position.maxScrollExtent,
|
||||||
|
|
@ -765,9 +768,7 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
|
||||||
_currentBot,
|
_currentBot,
|
||||||
widget.args
|
widget.args
|
||||||
.assistantsName !=
|
.assistantsName !=
|
||||||
null,
|
null);
|
||||||
webSearch:
|
|
||||||
_isSearchMode);
|
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
const Duration(
|
const Duration(
|
||||||
milliseconds:
|
milliseconds:
|
||||||
|
|
@ -878,31 +879,23 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
AiMessageBar(
|
AiMessageBar(
|
||||||
// قبلاً وقتی search mode روشن میشد bot به Gemini سویچ
|
bot: _isSearchMode && _searchBot != null
|
||||||
// میشد (legacy). الان وبجستجو بهعنوان tool روی هر
|
? _searchBot!
|
||||||
// bot انتخابشده اعمال میشود؛ پس همان bot فعلی را
|
: _getSelectedBot(),
|
||||||
// پاس میدهیم.
|
|
||||||
bot: _getSelectedBot(),
|
|
||||||
attch: widget.args.attach,
|
attch: widget.args.attach,
|
||||||
assistantsName: widget.args.assistantsName,
|
assistantsName: widget.args.assistantsName,
|
||||||
// toggle همیشه برای GPT/Gemini در دسترس باشد — نیازی
|
showSearchToggle: _currentBot.id == 35 && _searchBot != null,
|
||||||
// به _searchBot (= bot 36) نیست چون tool روی هر bot
|
|
||||||
// فعال است.
|
|
||||||
showSearchToggle: _currentBot.id == 35 || _currentBot.id == 36,
|
|
||||||
isSearchMode: _isSearchMode,
|
isSearchMode: _isSearchMode,
|
||||||
onSearchModeToggled: (bool isSearchOn) {
|
onSearchModeToggled: (bool isSearchOn) {
|
||||||
print('[SEARCH_TOGGLE] tapped — new mode=$isSearchOn');
|
if (_searchBot == null) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSearchMode = isSearchOn;
|
_isSearchMode = isSearchOn;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// فقط داشتن Gemini کافی است تا selector فعال شود.
|
showModelSelector: _currentBot.id == 35 &&
|
||||||
// Grok/Qwen در backend جدید نیستند ولی UI آنها را در
|
_geminiBot != null &&
|
||||||
// PopupMenu نگه میدارد (انتخاب میشوند ولی به همان
|
_grokBot != null &&
|
||||||
// GPT مپ میشوند چون bot model در DB انتخاب میشود
|
_qwenBot != null,
|
||||||
// نه از UI).
|
|
||||||
showModelSelector:
|
|
||||||
_currentBot.id == 35 && _geminiBot != null,
|
|
||||||
selectedModel: _selectedModel,
|
selectedModel: _selectedModel,
|
||||||
onModelChanged: (AiModel model) {
|
onModelChanged: (AiModel model) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
|
||||||
|
|
@ -162,9 +162,7 @@ class AiChatState extends CoreProvier {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> postMessage(BotsModel bot, bool isAssistants,
|
Future<void> postMessage(BotsModel bot, bool isAssistants) async {
|
||||||
{bool webSearch = false}) async {
|
|
||||||
print('[POST_MSG] bot.id=${bot.id} bot.name=${bot.name} webSearch=$webSearch');
|
|
||||||
onResponsing = true;
|
onResponsing = true;
|
||||||
final uploadedFile = file;
|
final uploadedFile = file;
|
||||||
file = null;
|
file = null;
|
||||||
|
|
@ -204,8 +202,7 @@ class AiChatState extends CoreProvier {
|
||||||
message: message,
|
message: message,
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
file: uploadedFile,
|
file: uploadedFile,
|
||||||
edite: isEdite,
|
edite: isEdite);
|
||||||
webSearch: webSearch);
|
|
||||||
file = null;
|
file = null;
|
||||||
isRecorded = false;
|
isRecorded = false;
|
||||||
update();
|
update();
|
||||||
|
|
@ -221,32 +218,16 @@ class AiChatState extends CoreProvier {
|
||||||
|
|
||||||
final r = res.listen((value) async {
|
final r = res.listen((value) async {
|
||||||
var str = utf8.decode(value);
|
var str = utf8.decode(value);
|
||||||
print(
|
|
||||||
'[CHAT_STREAM] chunk len=${str.length} hasTrailer=${str.contains('{{{')} preview="${str.substring(0, str.length.clamp(0, 80))}"');
|
|
||||||
|
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
|
if (bot.responseType != 'text') {
|
||||||
|
responseMessgae += str.split('{{{').first;
|
||||||
|
}
|
||||||
if (str.contains('{{{')) {
|
if (str.contains('{{{')) {
|
||||||
// اگر text و trailer در یک chunk merge شدند (که در پشت nginx
|
dataMessgae += "{{{${str.split('{{{').last}";
|
||||||
// معمولاً اتفاق میافتد)، باید **هر دو** را بگیریم — قسمت قبل از
|
|
||||||
// `{{{` متن پاسخ است، قسمت بعد از آن trailer شناسههاست.
|
|
||||||
final parts = str.split('{{{');
|
|
||||||
if (bot.responseType == 'text') {
|
|
||||||
responseMessgae += parts.first;
|
|
||||||
} else {
|
|
||||||
// برای bot های فایلمحور هم همان منطق قبلی.
|
|
||||||
responseMessgae += parts.first;
|
|
||||||
}
|
|
||||||
dataMessgae += "{{{${parts.last}";
|
|
||||||
print(
|
|
||||||
'[CHAT_STREAM] merged chunk - response.len=${responseMessgae.length} data.len=${dataMessgae.length}');
|
|
||||||
update();
|
update();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// chunk فقط متن دارد (هیچ trailer ندارد) — برای bot های file روی
|
|
||||||
// مسیر old حرکت میکنیم تا با اولین chunk file URL را بگیریم.
|
|
||||||
if (bot.responseType != 'text') {
|
|
||||||
responseMessgae += str;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bot.id == 101) {
|
if (bot.id == 101) {
|
||||||
|
|
@ -288,8 +269,6 @@ class AiChatState extends CoreProvier {
|
||||||
});
|
});
|
||||||
|
|
||||||
r.onDone(() async {
|
r.onDone(() async {
|
||||||
print(
|
|
||||||
'[CHAT_STREAM] onDone - responseMessgae.len=${responseMessgae.length} dataMessgae.len=${dataMessgae.length}');
|
|
||||||
if (chatId == null) {
|
if (chatId == null) {
|
||||||
final service = await getChatId();
|
final service = await getChatId();
|
||||||
navigatorKey.currentContext!.read<HistoryAiChatState>().getChats();
|
navigatorKey.currentContext!.read<HistoryAiChatState>().getChats();
|
||||||
|
|
@ -300,7 +279,6 @@ class AiChatState extends CoreProvier {
|
||||||
}
|
}
|
||||||
onResponsing = false;
|
onResponsing = false;
|
||||||
if (responseMessgae.isEmpty) {
|
if (responseMessgae.isEmpty) {
|
||||||
print('[CHAT_STREAM] responseMessgae empty - showing error');
|
|
||||||
messages.last.prompts.removeLast();
|
messages.last.prompts.removeLast();
|
||||||
messages.last.prompts.last =
|
messages.last.prompts.last =
|
||||||
messages.last.prompts.last.copyWith(error: true);
|
messages.last.prompts.last.copyWith(error: true);
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ class HistoryAiChatState extends CoreProvier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getBots() async {
|
Future<void> getBots() async {
|
||||||
print('[GET_BOTS] start');
|
|
||||||
appState = AppState.busy;
|
appState = AppState.busy;
|
||||||
loadingBots = true;
|
loadingBots = true;
|
||||||
update();
|
update();
|
||||||
|
|
@ -84,16 +83,12 @@ class HistoryAiChatState extends CoreProvier {
|
||||||
RequestHelper.aiBots(),
|
RequestHelper.aiBots(),
|
||||||
);
|
);
|
||||||
await service.httpGet();
|
await service.httpGet();
|
||||||
print(
|
|
||||||
'[GET_BOTS] success=${service.isSuccess} status=${service.statusCode}');
|
|
||||||
if (service.isSuccess) {
|
if (service.isSuccess) {
|
||||||
bots.clear();
|
bots.clear();
|
||||||
final messages = service.result['bots'];
|
final messages = service.result['bots'];
|
||||||
print('[GET_BOTS] messages type=${messages.runtimeType} length=${messages?.length}');
|
|
||||||
for (var i = 0; i < messages.length; i++) {
|
for (var i = 0; i < messages.length; i++) {
|
||||||
bots.add(BotsModel.fromJson(messages[i]));
|
bots.add(BotsModel.fromJson(messages[i]));
|
||||||
}
|
}
|
||||||
print('[GET_BOTS] bots populated, length=${bots.length}');
|
|
||||||
bot = bots.first;
|
bot = bots.first;
|
||||||
appState = AppState.idle;
|
appState = AppState.idle;
|
||||||
loadingBots = false;
|
loadingBots = false;
|
||||||
|
|
@ -102,8 +97,6 @@ class HistoryAiChatState extends CoreProvier {
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
print(
|
|
||||||
'[GET_BOTS] FAILED — result=${service.result?.toString().substring(0, service.result?.toString().length.clamp(0, 200))}');
|
|
||||||
appState = AppState.failed;
|
appState = AppState.failed;
|
||||||
loadingBots = false;
|
loadingBots = false;
|
||||||
update();
|
update();
|
||||||
|
|
|
||||||
|
|
@ -794,8 +794,7 @@ class _AiMessageBarState extends State<AiMessageBar> {
|
||||||
openAttach = false;
|
openAttach = false;
|
||||||
state.update();
|
state.update();
|
||||||
await state.postMessage(
|
await state.postMessage(
|
||||||
widget.bot, widget.assistantsName != null,
|
widget.bot, widget.assistantsName != null);
|
||||||
webSearch: widget.isSearchMode);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:didvan/config/design_config.dart';
|
||||||
import 'package:didvan/config/theme_data.dart';
|
import 'package:didvan/config/theme_data.dart';
|
||||||
import 'package:didvan/constants/assets.dart';
|
import 'package:didvan/constants/assets.dart';
|
||||||
import 'package:didvan/models/ai/ai_chat_args.dart';
|
import 'package:didvan/models/ai/ai_chat_args.dart';
|
||||||
import 'package:didvan/models/ai/bots_model.dart';
|
|
||||||
import 'package:didvan/providers/user.dart';
|
import 'package:didvan/providers/user.dart';
|
||||||
import 'package:didvan/views/ai/history_ai_chat_state.dart';
|
import 'package:didvan/views/ai/history_ai_chat_state.dart';
|
||||||
import 'package:didvan/views/widgets/didvan/text.dart';
|
import 'package:didvan/views/widgets/didvan/text.dart';
|
||||||
|
|
@ -25,96 +24,6 @@ class HoshanHomeAppBar extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HoshanHomeAppBarState extends State<HoshanHomeAppBar> {
|
class _HoshanHomeAppBarState extends State<HoshanHomeAppBar> {
|
||||||
/// مدلی که کاربر برای شروع چت بعدی انتخاب کرده. اگر null باشد، اولین
|
|
||||||
/// bot لیست (پیشفرض = GPT 5.5) استفاده میشود.
|
|
||||||
BotsModel? _selectedBot;
|
|
||||||
|
|
||||||
/// منوی انتخاب مدل را بهصورت BottomSheet نشان میدهد و انتخاب کاربر
|
|
||||||
/// را در state ذخیره میکند.
|
|
||||||
Future<void> _pickBot(BuildContext context) async {
|
|
||||||
final historyState = context.read<HistoryAiChatState>();
|
|
||||||
if (historyState.bots.isEmpty) {
|
|
||||||
await historyState.getBots();
|
|
||||||
}
|
|
||||||
if (historyState.bots.isEmpty || !context.mounted) return;
|
|
||||||
|
|
||||||
final selected = await showModalBottomSheet<BotsModel>(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
|
||||||
builder: (ctx) {
|
|
||||||
return SafeArea(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
width: 36,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade400,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
DidvanText(
|
|
||||||
'انتخاب مدل هوش مصنوعی',
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).colorScheme.title,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
for (final bot in historyState.bots)
|
|
||||||
ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
radius: 18,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
backgroundImage: (bot.image ?? '').isNotEmpty
|
|
||||||
? NetworkImage(bot.image!)
|
|
||||||
: null,
|
|
||||||
child: (bot.image ?? '').isEmpty
|
|
||||||
? const Icon(Icons.smart_toy)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
title: DidvanText(
|
|
||||||
bot.short ?? bot.name ?? 'مدل',
|
|
||||||
color: Theme.of(context).colorScheme.title,
|
|
||||||
),
|
|
||||||
subtitle: bot.description != null
|
|
||||||
? DidvanText(
|
|
||||||
bot.description!,
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).colorScheme.caption,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
trailing: (_selectedBot?.id ?? historyState.bots.first.id) ==
|
|
||||||
bot.id
|
|
||||||
? const Icon(Icons.check_circle, color: Colors.green)
|
|
||||||
: null,
|
|
||||||
onTap: () => Navigator.of(ctx).pop(bot),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (selected != null && mounted) {
|
|
||||||
setState(() => _selectedBot = selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// مدل فعلی (انتخاب کاربر یا اولین bot موجود).
|
|
||||||
BotsModel? _currentBot(HistoryAiChatState state) {
|
|
||||||
if (_selectedBot != null) return _selectedBot;
|
|
||||||
if (state.bots.isNotEmpty) return state.bots.first;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// String? _welcomeMessage;
|
// String? _welcomeMessage;
|
||||||
// bool _isLoadingWelcome = true;
|
// bool _isLoadingWelcome = true;
|
||||||
|
|
||||||
|
|
@ -151,27 +60,27 @@ class _HoshanHomeAppBarState extends State<HoshanHomeAppBar> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// "سوال خود را بپرسید" → ایجاد چت جدید با مدل انتخابشدهٔ کاربر
|
void _startNewChat(BuildContext context) {
|
||||||
/// (یا اولین bot لیست بهعنوان پیشفرض).
|
|
||||||
Future<void> _startNewChat(BuildContext context) async {
|
|
||||||
final historyState = context.read<HistoryAiChatState>();
|
final historyState = context.read<HistoryAiChatState>();
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
|
void navigateToNewChat() {
|
||||||
|
if (historyState.bots.isNotEmpty) {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
Routes.aiChat,
|
||||||
|
arguments: AiChatArgs(
|
||||||
|
bot: historyState.bots.first,
|
||||||
|
isTool: historyState.bots,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (historyState.bots.isEmpty) {
|
if (historyState.bots.isEmpty) {
|
||||||
await historyState.getBots();
|
historyState.getBots();
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), navigateToNewChat);
|
||||||
|
} else {
|
||||||
|
navigateToNewChat();
|
||||||
}
|
}
|
||||||
if (historyState.bots.isEmpty || !context.mounted) return;
|
|
||||||
|
|
||||||
final bot = _selectedBot ?? historyState.bots.first;
|
|
||||||
try {
|
|
||||||
navigator.pushNamed(
|
|
||||||
Routes.aiChat,
|
|
||||||
arguments: AiChatArgs(
|
|
||||||
bot: bot,
|
|
||||||
isTool: historyState.bots,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 5.5.0+8008
|
version: 5.2.0+8008
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue