Skip to content

Commit

Permalink
Refactor chat screen using clean architecture
Browse files Browse the repository at this point in the history
  • Loading branch information
banghuazhao committed Sep 28, 2024
1 parent 9c14443 commit e67079e
Show file tree
Hide file tree
Showing 26 changed files with 625 additions and 369 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ ios/fastlane/*p8
ios/fastlane/.env
ios/fastlane/report.xml

lib/home/Chat/chat_config.dart
lib/data/core/chat_config.dart

coverage
7 changes: 7 additions & 0 deletions lib/data/core/api_constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:swiftcomp/data/core/chat_config.dart';

class ApiConstants {
static const String baseUrl = 'https://api.openai.com/v1';
static const String chatEndpoint = '$baseUrl/chat/completions';
static String apiKey = ChatConfig.apiKey;
}
5 changes: 5 additions & 0 deletions lib/data/core/network_exceptions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class NetworkException implements Exception {
final String message;

NetworkException(this.message);
}
66 changes: 66 additions & 0 deletions lib/data/data_sources/chat_remote_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../domain/entities/chat_session.dart';
import '../../domain/entities/message.dart';
import '../core/api_constants.dart';
import '../core/network_exceptions.dart';

abstract class ChatRemoteDataSource {
Stream<String> sendMessages(List<Message> messages);
}

class ChatRemoteDataSourceImpl implements ChatRemoteDataSource {
final http.Client client;

ChatRemoteDataSourceImpl({required this.client});

@override
Stream<String> sendMessages(List<Message> messages) async* {
final request = http.Request('POST', Uri.parse(ApiConstants.chatEndpoint))
..headers.addAll({
'Content-Type': 'application/json',
'Authorization': 'Bearer ${ApiConstants.apiKey}',
})
..body = jsonEncode({
"model": "gpt-4o",
"stream": true,
'messages': messages,
});

final response = await client.send(request);

if (response.statusCode == 200) {
final utf8DecodedStream = response.stream.transform(utf8.decoder);
String buffer = '';

await for (var chunk in utf8DecodedStream) {
buffer += chunk;
List<String> lines = buffer.split('\n');
buffer = lines.removeLast();

for (var line in lines) {
if (line.startsWith('data:')) {
var jsonString = line.replaceFirst('data: ', '').trim();

if (jsonString == '[DONE]') {
return;
}

try {
final data = jsonDecode(jsonString);
final content = data['choices'][0]['delta']['content'] ?? '';
if (content.isNotEmpty) {
yield content;
}
} catch (_) {
// Handle JSON parsing errors
continue;
}
}
}
}
} else {
throw NetworkException('Failed to send message: ${response.statusCode}');
}
}
}
15 changes: 15 additions & 0 deletions lib/data/repositories/chat_repository_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import '../../domain/entities/chat_session.dart';
import '../../domain/entities/message.dart';
import '../../domain/repositories_abstract/chat_repository.dart';
import '../data_sources/chat_remote_data_source.dart';

class ChatRepositoryImp implements ChatRepository {
final ChatRemoteDataSource remoteDataSource;

ChatRepositoryImp({required this.remoteDataSource});

@override
Stream<String> sendMessages(List<Message> messages) {
return remoteDataSource.sendMessages(messages);
}
}
30 changes: 30 additions & 0 deletions lib/data/repositories/chat_session_repository_imp.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import '../../domain/entities/chat_session.dart';
import '../../domain/repositories_abstract/chat_session_repository.dart';

class ChatSessionRepositoryImpl implements ChatSessionRepository {
@override
Future<List<ChatSession>> getAllSessions() async {
// Fetch sessions from data source (e.g., local storage, API)
return [];
}

@override
Future<void> createSession(ChatSession session) async {
// Create session to a data source
}

@override
Future<void> saveSession(ChatSession session) async {
// Save session to a data source
}

@override
Future<void> deleteSession(String id) async {
// Delete session from data source
}

@override
Future<ChatSession?> selectSession(String id) async {
return null;
}
}
33 changes: 33 additions & 0 deletions lib/domain/entities/chat_session.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'message.dart';

class ChatSession {
final String id;
final String title;
List<Message> messages;

// Constructor
ChatSession({
required this.id,
required this.title,
List<Message>? messages, // Optional parameter for messages
}) : messages = messages ?? []; // Initialize messages as an empty list if null

factory ChatSession.fromJson(Map<String, dynamic> json) {
var messagesJson = json['messages'] as List;
List<Message> messages = messagesJson.map((msg) => Message.fromJson(msg)).toList();

return ChatSession(
id: json['id'],
title: json['title'],
messages: messages,
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'messages': messages.map((msg) => (msg as Message).toJson()).toList(),
};
}
}
22 changes: 22 additions & 0 deletions lib/domain/entities/message.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class Message {
final String role;
String content;

Message({required this.role, required this.content});

// Factory constructor for creating a new Message instance from JSON
factory Message.fromJson(Map<String, dynamic> json) {
return Message(
role: json['role'],
content: json['content'],
);
}

// Method for converting a Message instance to JSON format
Map<String, dynamic> toJson() {
return {
'role': role,
'content': content,
};
}
}
5 changes: 5 additions & 0 deletions lib/domain/repositories_abstract/chat_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import '../entities/message.dart';

abstract class ChatRepository {
Stream<String> sendMessages(List<Message> messages);
}
9 changes: 9 additions & 0 deletions lib/domain/repositories_abstract/chat_session_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import '../entities/chat_session.dart';

abstract class ChatSessionRepository {
Future<List<ChatSession>> getAllSessions(); // Fetch sessions from a data source
Future<void> createSession(ChatSession session);
Future<void> saveSession(ChatSession session);
Future<void> deleteSession(String id);
Future<ChatSession?> selectSession(String id);
}
53 changes: 53 additions & 0 deletions lib/domain/usecases/chat_session_usecase.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import '../entities/chat_session.dart';
import '../entities/message.dart';
import '../repositories_abstract/chat_session_repository.dart';

class ChatSessionUseCase {
final ChatSessionRepository repository;

ChatSessionUseCase(this.repository);

Future<List<ChatSession>> getAllSessions() async {
return repository.getAllSessions();
}

Future<void> saveSession(ChatSession session) async {
// You can add business rules here (e.g., session validation)
repository.saveSession(session);
}

Future<void> deleteSession(String sessionId) async {
repository.deleteSession(sessionId);
}

@override
Future<void> createSession(ChatSession session) async {
repository.createSession(session);
}

void addMessageToSession(ChatSession session, Message message) {
session.messages.add(message);
saveSession(session);
}

// Check if the last message in the session is from the assistant
bool isLastMessageAssistInSession(ChatSession session) {
final messages = session.messages;
if (messages != null && messages.isNotEmpty) {
return messages.last.role == 'assistant';
}
return false;
}

// Add a new method to update the last assistant message
void updateLastAssistantMessage(ChatSession session, String token) {
// Find the last message that is from the assistant
for (var i = session.messages.length - 1; i >= 0; i--) {
if (session.messages[i].role == 'assistant') {
final previousContent = session.messages[i].content;
session.messages[i] = Message(role: 'assistant', content: previousContent + token);
break;
}
}
}
}
12 changes: 12 additions & 0 deletions lib/domain/usecases/chat_usecase.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import '../entities/message.dart';
import '../repositories_abstract/chat_repository.dart';

class ChatUseCase {
final ChatRepository repository;

ChatUseCase(this.repository);

Stream<String> sendMessages(List<Message> messages) {
return repository.sendMessages(messages);
}
}
Loading

0 comments on commit e67079e

Please sign in to comment.