mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
Backend fixes: - Fix duplicate AssistantResponseJob triggering causing duplicate AI responses - UserMessage model already handles job triggering via after_create_commit callback - Remove redundant job enqueue in chats_controller and messages_controller Mobile app features: - Implement complete AI chat interface and conversation management - Add Chat, Message, and ToolCall data models - Add ChatProvider for state management with polling mechanism - Add ChatService to handle all chat-related API requests - Add chat list screen (ChatListScreen) - Add conversation detail screen (ChatConversationScreen) - Refactor navigation structure with bottom navigation bar (MainNavigationScreen) - Add settings screen (SettingsScreen) - Optimize TransactionsProvider to support account filtering Technical details: - Implement message polling mechanism for real-time AI responses - Support chat creation, deletion, retry and other operations - Integrate Material Design 3 design language - Improve user experience and error handling Co-authored-by: dwvwdv <dwvwdv@protonmail.com>
302 lines
7.5 KiB
Dart
302 lines
7.5 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../models/chat.dart';
|
|
import '../models/message.dart';
|
|
import '../services/chat_service.dart';
|
|
|
|
class ChatProvider with ChangeNotifier {
|
|
final ChatService _chatService = ChatService();
|
|
|
|
List<Chat> _chats = [];
|
|
Chat? _currentChat;
|
|
bool _isLoading = false;
|
|
bool _isSendingMessage = false;
|
|
String? _errorMessage;
|
|
Timer? _pollingTimer;
|
|
|
|
List<Chat> get chats => _chats;
|
|
Chat? get currentChat => _currentChat;
|
|
bool get isLoading => _isLoading;
|
|
bool get isSendingMessage => _isSendingMessage;
|
|
String? get errorMessage => _errorMessage;
|
|
|
|
/// Fetch list of chats
|
|
Future<void> fetchChats({
|
|
required String accessToken,
|
|
int page = 1,
|
|
int perPage = 25,
|
|
}) async {
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final result = await _chatService.getChats(
|
|
accessToken: accessToken,
|
|
page: page,
|
|
perPage: perPage,
|
|
);
|
|
|
|
if (result['success'] == true) {
|
|
_chats = result['chats'] as List<Chat>;
|
|
_errorMessage = null;
|
|
} else {
|
|
_errorMessage = result['error'] ?? 'Failed to fetch chats';
|
|
}
|
|
} catch (e) {
|
|
_errorMessage = 'Error: ${e.toString()}';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Fetch a specific chat with messages
|
|
Future<void> fetchChat({
|
|
required String accessToken,
|
|
required String chatId,
|
|
}) async {
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final result = await _chatService.getChat(
|
|
accessToken: accessToken,
|
|
chatId: chatId,
|
|
);
|
|
|
|
if (result['success'] == true) {
|
|
_currentChat = result['chat'] as Chat;
|
|
_errorMessage = null;
|
|
} else {
|
|
_errorMessage = result['error'] ?? 'Failed to fetch chat';
|
|
}
|
|
} catch (e) {
|
|
_errorMessage = 'Error: ${e.toString()}';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Create a new chat
|
|
Future<Chat?> createChat({
|
|
required String accessToken,
|
|
String? title,
|
|
String? initialMessage,
|
|
String model = 'gpt-4',
|
|
}) async {
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final result = await _chatService.createChat(
|
|
accessToken: accessToken,
|
|
title: title,
|
|
initialMessage: initialMessage,
|
|
model: model,
|
|
);
|
|
|
|
if (result['success'] == true) {
|
|
final chat = result['chat'] as Chat;
|
|
_currentChat = chat;
|
|
_chats.insert(0, chat);
|
|
_errorMessage = null;
|
|
|
|
// Start polling for AI response if initial message was sent
|
|
if (initialMessage != null) {
|
|
_startPolling(accessToken, chat.id);
|
|
}
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
return chat;
|
|
} else {
|
|
_errorMessage = result['error'] ?? 'Failed to create chat';
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
_errorMessage = 'Error: ${e.toString()}';
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Send a message to the current chat
|
|
Future<void> sendMessage({
|
|
required String accessToken,
|
|
required String chatId,
|
|
required String content,
|
|
}) async {
|
|
_isSendingMessage = true;
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final result = await _chatService.sendMessage(
|
|
accessToken: accessToken,
|
|
chatId: chatId,
|
|
content: content,
|
|
);
|
|
|
|
if (result['success'] == true) {
|
|
final message = result['message'] as Message;
|
|
|
|
// Add the message to current chat if it's loaded
|
|
if (_currentChat != null && _currentChat!.id == chatId) {
|
|
_currentChat = _currentChat!.copyWith(
|
|
messages: [..._currentChat!.messages, message],
|
|
);
|
|
}
|
|
|
|
_errorMessage = null;
|
|
|
|
// Start polling for AI response
|
|
_startPolling(accessToken, chatId);
|
|
} else {
|
|
_errorMessage = result['error'] ?? 'Failed to send message';
|
|
}
|
|
} catch (e) {
|
|
_errorMessage = 'Error: ${e.toString()}';
|
|
} finally {
|
|
_isSendingMessage = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Update chat title
|
|
Future<void> updateChatTitle({
|
|
required String accessToken,
|
|
required String chatId,
|
|
required String title,
|
|
}) async {
|
|
try {
|
|
final result = await _chatService.updateChat(
|
|
accessToken: accessToken,
|
|
chatId: chatId,
|
|
title: title,
|
|
);
|
|
|
|
if (result['success'] == true) {
|
|
final updatedChat = result['chat'] as Chat;
|
|
|
|
// Update in the list
|
|
final index = _chats.indexWhere((c) => c.id == chatId);
|
|
if (index != -1) {
|
|
_chats[index] = updatedChat;
|
|
}
|
|
|
|
// Update current chat if it's the same
|
|
if (_currentChat != null && _currentChat!.id == chatId) {
|
|
_currentChat = updatedChat;
|
|
}
|
|
|
|
notifyListeners();
|
|
}
|
|
} catch (e) {
|
|
_errorMessage = 'Error: ${e.toString()}';
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Delete a chat
|
|
Future<bool> deleteChat({
|
|
required String accessToken,
|
|
required String chatId,
|
|
}) async {
|
|
try {
|
|
final result = await _chatService.deleteChat(
|
|
accessToken: accessToken,
|
|
chatId: chatId,
|
|
);
|
|
|
|
if (result['success'] == true) {
|
|
_chats.removeWhere((c) => c.id == chatId);
|
|
|
|
if (_currentChat != null && _currentChat!.id == chatId) {
|
|
_currentChat = null;
|
|
}
|
|
|
|
notifyListeners();
|
|
return true;
|
|
} else {
|
|
_errorMessage = result['error'] ?? 'Failed to delete chat';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
_errorMessage = 'Error: ${e.toString()}';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Start polling for new messages (AI responses)
|
|
void _startPolling(String accessToken, String chatId) {
|
|
_stopPolling();
|
|
|
|
_pollingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
|
|
await _pollForUpdates(accessToken, chatId);
|
|
});
|
|
}
|
|
|
|
/// Stop polling
|
|
void _stopPolling() {
|
|
_pollingTimer?.cancel();
|
|
_pollingTimer = null;
|
|
}
|
|
|
|
/// Poll for updates
|
|
Future<void> _pollForUpdates(String accessToken, String chatId) async {
|
|
try {
|
|
final result = await _chatService.getChat(
|
|
accessToken: accessToken,
|
|
chatId: chatId,
|
|
);
|
|
|
|
if (result['success'] == true) {
|
|
final updatedChat = result['chat'] as Chat;
|
|
|
|
// Check if we have new messages
|
|
if (_currentChat != null && _currentChat!.id == chatId) {
|
|
final oldMessageCount = _currentChat!.messages.length;
|
|
final newMessageCount = updatedChat.messages.length;
|
|
|
|
if (newMessageCount > oldMessageCount) {
|
|
_currentChat = updatedChat;
|
|
notifyListeners();
|
|
|
|
// Check if the last message is from assistant and complete
|
|
final lastMessage = updatedChat.messages.lastOrNull;
|
|
if (lastMessage != null && lastMessage.isAssistant) {
|
|
// Stop polling after getting assistant response
|
|
_stopPolling();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Silently fail polling errors to avoid interrupting user experience
|
|
debugPrint('Polling error: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// Clear current chat
|
|
void clearCurrentChat() {
|
|
_currentChat = null;
|
|
_stopPolling();
|
|
notifyListeners();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_stopPolling();
|
|
super.dispose();
|
|
}
|
|
}
|