Compare commits

..

No commits in common. "e30b01a3bb682719efe14c15d23b0d082bc4e155" and "61718baa54cb3506702d53ec6abdec6e94620260" have entirely different histories.

9 changed files with 79 additions and 266 deletions

View File

@ -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 معتبر است، همان را برمیگردانیم.
if (TokenStorage.isAccessTokenValid) {
final cached = TokenStorage.accessToken; final cached = TokenStorage.accessToken;
if (cached != null && cached.isNotEmpty) { if (cached != null && cached.isNotEmpty) {
RequestService.token = cached; RequestService.token = cached;
return 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;
} }
} }

View File

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

View File

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

View File

@ -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 دنبال میشد
// (الگوی legacy multi-model). در ساختار جدید Gemini یک bot
// مستقل با id=36 است.
try {
_searchBot = bots.firstWhere((b) => b.id == 36); _searchBot = bots.firstWhere((b) => b.id == 36);
} catch (_) {
_searchBot = null;
}
try { try {
_geminiBot = bots.firstWhere( _geminiBot = bots.firstWhere((b) =>
(b) => (b.name?.toLowerCase().contains('gemini') ?? false)); b.id == 35 &&
(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(() {

View File

@ -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 (str.contains('{{{')) { if (bot.responseType != 'text') {
// اگر text و trailer در یک chunk merge شدند (که در پشت nginx responseMessgae += str.split('{{{').first;
// معمولاً اتفاق میافتد)، باید **هر دو** را بگیریم قسمت قبل از
// `{{{` متن پاسخ است، قسمت بعد از آن trailer شناسههاست.
final parts = str.split('{{{');
if (bot.responseType == 'text') {
responseMessgae += parts.first;
} else {
// برای bot های فایلمحور هم همان منطق قبلی.
responseMessgae += parts.first;
} }
dataMessgae += "{{{${parts.last}"; if (str.contains('{{{')) {
print( dataMessgae += "{{{${str.split('{{{').last}";
'[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);

View File

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

View File

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

View File

@ -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);
if (historyState.bots.isEmpty) { void navigateToNewChat() {
await historyState.getBots(); if (historyState.bots.isNotEmpty) {
} Navigator.of(context).pushNamed(
if (historyState.bots.isEmpty || !context.mounted) return;
final bot = _selectedBot ?? historyState.bots.first;
try {
navigator.pushNamed(
Routes.aiChat, Routes.aiChat,
arguments: AiChatArgs( arguments: AiChatArgs(
bot: bot, bot: historyState.bots.first,
isTool: historyState.bots, isTool: historyState.bots,
), ),
); );
} catch (_) {} }
}
if (historyState.bots.isEmpty) {
historyState.getBots();
Future.delayed(const Duration(milliseconds: 500), navigateToNewChat);
} else {
navigateToNewChat();
}
} }
@override @override

View File

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