mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
feat: implement mobile AI chat feature and fix duplicate response issue (#610)
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>
This commit is contained in:
77
mobile/lib/models/chat.dart
Normal file
77
mobile/lib/models/chat.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'message.dart';
|
||||
|
||||
class Chat {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? error;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final List<Message> messages;
|
||||
final int? messageCount;
|
||||
final DateTime? lastMessageAt;
|
||||
|
||||
Chat({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.error,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.messages = const [],
|
||||
this.messageCount,
|
||||
this.lastMessageAt,
|
||||
});
|
||||
|
||||
factory Chat.fromJson(Map<String, dynamic> json) {
|
||||
return Chat(
|
||||
id: json['id'].toString(),
|
||||
title: json['title'] as String,
|
||||
error: json['error'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
messages: json['messages'] != null
|
||||
? (json['messages'] as List)
|
||||
.map((m) => Message.fromJson(m as Map<String, dynamic>))
|
||||
.toList()
|
||||
: [],
|
||||
messageCount: json['message_count'] as int?,
|
||||
lastMessageAt: json['last_message_at'] != null
|
||||
? DateTime.parse(json['last_message_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'error': error,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'messages': messages.map((m) => m.toJson()).toList(),
|
||||
'message_count': messageCount,
|
||||
'last_message_at': lastMessageAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Chat copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? error,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<Message>? messages,
|
||||
int? messageCount,
|
||||
DateTime? lastMessageAt,
|
||||
}) {
|
||||
return Chat(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
error: error ?? this.error,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
messages: messages ?? this.messages,
|
||||
messageCount: messageCount ?? this.messageCount,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
56
mobile/lib/models/message.dart
Normal file
56
mobile/lib/models/message.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'tool_call.dart';
|
||||
|
||||
class Message {
|
||||
final String id;
|
||||
final String type;
|
||||
final String role;
|
||||
final String content;
|
||||
final String? model;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final List<ToolCall>? toolCalls;
|
||||
|
||||
Message({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.role,
|
||||
required this.content,
|
||||
this.model,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.toolCalls,
|
||||
});
|
||||
|
||||
factory Message.fromJson(Map<String, dynamic> json) {
|
||||
return Message(
|
||||
id: json['id'].toString(),
|
||||
type: json['type'] as String,
|
||||
role: json['role'] as String,
|
||||
content: json['content'] as String,
|
||||
model: json['model'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
toolCalls: json['tool_calls'] != null
|
||||
? (json['tool_calls'] as List)
|
||||
.map((tc) => ToolCall.fromJson(tc as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type,
|
||||
'role': role,
|
||||
'content': content,
|
||||
'model': model,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'tool_calls': toolCalls?.map((tc) => tc.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
bool get isUser => role == 'user';
|
||||
bool get isAssistant => role == 'assistant';
|
||||
}
|
||||
53
mobile/lib/models/tool_call.dart
Normal file
53
mobile/lib/models/tool_call.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class ToolCall {
|
||||
final String id;
|
||||
final String functionName;
|
||||
final Map<String, dynamic> functionArguments;
|
||||
final Map<String, dynamic>? functionResult;
|
||||
final DateTime createdAt;
|
||||
|
||||
ToolCall({
|
||||
required this.id,
|
||||
required this.functionName,
|
||||
required this.functionArguments,
|
||||
this.functionResult,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ToolCall.fromJson(Map<String, dynamic> json) {
|
||||
return ToolCall(
|
||||
id: json['id'].toString(),
|
||||
functionName: json['function_name'] as String,
|
||||
functionArguments: _parseJsonField(json['function_arguments']),
|
||||
functionResult: json['function_result'] != null
|
||||
? _parseJsonField(json['function_result'])
|
||||
: null,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _parseJsonField(dynamic field) {
|
||||
if (field == null) return {};
|
||||
if (field is Map<String, dynamic>) return field;
|
||||
if (field is String) {
|
||||
try {
|
||||
final parsed = jsonDecode(field);
|
||||
return parsed is Map<String, dynamic> ? parsed : {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'function_name': functionName,
|
||||
'function_arguments': functionArguments,
|
||||
'function_result': functionResult,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user